+2
-1
README.md
+2
-1
README.md
···
79
79
- A character device entry with major 0 and minor 0 is treated as a "whiteout marker" (following [unionfs][whiteout]): it causes any existing file or directory with the same name to be deleted.
80
80
- A directory entry replaces any existing file or directory with the same name (if any), recursively removing the old contents.
81
81
- A file or symlink entry replaces any existing file or directory with the same name (if any).
82
-
- In any case, the parent of an entry must exist and be a directory.
82
+
- If there is no `Create-Parents:` header or a `Create-Parents: no` header is present, the parent path of an entry must exist and refer to a directory.
83
+
- If a `Create-Parents: yes` header is present, any missing segments in the parent path of an entry will be created (like `mkdir -p`). Any existing segments refer to directories.
83
84
- The request must have a `Atomic: yes` or `Atomic: no` header. Not every backend configuration makes it possible to perform atomic compare-and-swap operations; on backends without atomic CAS support, `Atomic: yes` requests will fail, while `Atomic: no` requests will provide a best-effort approximation.
84
85
- If a `PATCH` request loses a race against another content update request, it may return `409 Conflict`. This is true regardless of the `Atomic:` header value. Whenever this happens, resubmit the request as-is.
85
86
- If the site has no contents after the update is applied, performs the same action as `DELETE`.
+16
-5
src/pages.go
+16
-5
src/pages.go
···
452
452
return err
453
453
}
454
454
455
-
updateCtx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout))
455
+
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout))
456
456
defer cancel()
457
457
458
458
contentType := getMediaType(r.Header.Get("Content-Type"))
···
486
486
return nil
487
487
}
488
488
489
-
result = UpdateFromRepository(updateCtx, webRoot, repoURL, branch)
489
+
result = UpdateFromRepository(ctx, webRoot, repoURL, branch)
490
490
491
491
default:
492
492
_, err := AuthorizeUpdateFromArchive(r)
···
500
500
501
501
// request body contains archive
502
502
reader := http.MaxBytesReader(w, r.Body, int64(config.Limits.MaxSiteSize.Bytes()))
503
-
result = UpdateFromArchive(updateCtx, webRoot, contentType, reader)
503
+
result = UpdateFromArchive(ctx, webRoot, contentType, reader)
504
504
}
505
505
506
506
return reportUpdateResult(w, result)
···
554
554
return nil
555
555
}
556
556
557
-
updateCtx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout))
557
+
var parents CreateParentsMode
558
+
switch r.Header.Get("Create-Parents") {
559
+
case "", "no":
560
+
parents = RequireParents
561
+
case "yes":
562
+
parents = CreateParents
563
+
default:
564
+
http.Error(w, "malformed Create-Parents: header", http.StatusBadRequest)
565
+
return nil
566
+
}
567
+
568
+
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout))
558
569
defer cancel()
559
570
560
571
contentType := getMediaType(r.Header.Get("Content-Type"))
561
572
reader := http.MaxBytesReader(w, r.Body, int64(config.Limits.MaxSiteSize.Bytes()))
562
-
result := PartialUpdateFromArchive(updateCtx, webRoot, contentType, reader)
573
+
result := PartialUpdateFromArchive(ctx, webRoot, contentType, reader, parents)
563
574
return reportUpdateResult(w, result)
564
575
}
565
576
+19
-5
src/patch.go
+19
-5
src/patch.go
···
12
12
13
13
var ErrMalformedPatch = errors.New("malformed patch")
14
14
15
+
type CreateParentsMode int
16
+
17
+
const (
18
+
RequireParents CreateParentsMode = iota
19
+
CreateParents
20
+
)
21
+
15
22
// Mutates `manifest` according to a tar stream and the following rules:
16
23
// - A character device with major 0 and minor 0 is a "whiteout marker". When placed
17
24
// at a given path, this path and its entire subtree (if any) are removed from the manifest.
18
25
// - When a directory is placed at a given path, this path and its entire subtree (if any) are
19
26
// removed from the manifest and replaced with the contents of the directory.
20
-
func ApplyTarPatch(manifest *Manifest, reader io.Reader) error {
27
+
func ApplyTarPatch(manifest *Manifest, reader io.Reader, parents CreateParentsMode) error {
21
28
type Node struct {
22
29
entry *Entry
23
30
children map[string]*Node
···
72
79
return fmt.Errorf("%w: %s: not a directory", ErrMalformedPatch, dirName)
73
80
}
74
81
if _, exists := node.children[segment]; !exists {
75
-
nodeName := strings.Join(segments[:index+1], "/")
76
-
return fmt.Errorf("%w: %s: path not found", ErrMalformedPatch, nodeName)
77
-
} else {
78
-
node = node.children[segment]
82
+
switch parents {
83
+
case RequireParents:
84
+
nodeName := strings.Join(segments[:index+1], "/")
85
+
return fmt.Errorf("%w: %s: path not found", ErrMalformedPatch, nodeName)
86
+
case CreateParents:
87
+
node.children[segment] = &Node{
88
+
entry: NewManifestEntry(Type_Directory, nil),
89
+
children: map[string]*Node{},
90
+
}
91
+
}
79
92
}
93
+
node = node.children[segment]
80
94
}
81
95
if node.children == nil {
82
96
dirName := strings.Join(segments[:len(segments)-1], "/")
+2
-1
src/update.go
+2
-1
src/update.go
···
159
159
webRoot string,
160
160
contentType string,
161
161
reader io.Reader,
162
+
parents CreateParentsMode,
162
163
) (result UpdateResult) {
163
164
var err error
164
165
···
177
178
// `*Manifest` objects, which should never be mutated.
178
179
newManifest := &Manifest{}
179
180
proto.Merge(newManifest, oldManifest)
180
-
if err := ApplyTarPatch(newManifest, reader); err != nil {
181
+
if err := ApplyTarPatch(newManifest, reader, parents); err != nil {
181
182
return nil, err
182
183
} else {
183
184
return newManifest, nil