+2
-1
README.md
+2
-1
README.md
···
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
- A directory entry replaces any existing file or directory with the same name (if any), recursively removing the old contents.
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.
83
- 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
- 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
- If the site has no contents after the update is applied, performs the same action as `DELETE`.
···
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
- A directory entry replaces any existing file or directory with the same name (if any), recursively removing the old contents.
81
- A file or symlink entry replaces any existing file or directory with the same name (if any).
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.
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.
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.
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
return err
453
}
454
455
-
updateCtx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout))
456
defer cancel()
457
458
contentType := getMediaType(r.Header.Get("Content-Type"))
···
486
return nil
487
}
488
489
-
result = UpdateFromRepository(updateCtx, webRoot, repoURL, branch)
490
491
default:
492
_, err := AuthorizeUpdateFromArchive(r)
···
500
501
// request body contains archive
502
reader := http.MaxBytesReader(w, r.Body, int64(config.Limits.MaxSiteSize.Bytes()))
503
-
result = UpdateFromArchive(updateCtx, webRoot, contentType, reader)
504
}
505
506
return reportUpdateResult(w, result)
···
554
return nil
555
}
556
557
-
updateCtx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout))
558
defer cancel()
559
560
contentType := getMediaType(r.Header.Get("Content-Type"))
561
reader := http.MaxBytesReader(w, r.Body, int64(config.Limits.MaxSiteSize.Bytes()))
562
-
result := PartialUpdateFromArchive(updateCtx, webRoot, contentType, reader)
563
return reportUpdateResult(w, result)
564
}
565
···
452
return err
453
}
454
455
+
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(config.Limits.UpdateTimeout))
456
defer cancel()
457
458
contentType := getMediaType(r.Header.Get("Content-Type"))
···
486
return nil
487
}
488
489
+
result = UpdateFromRepository(ctx, webRoot, repoURL, branch)
490
491
default:
492
_, err := AuthorizeUpdateFromArchive(r)
···
500
501
// request body contains archive
502
reader := http.MaxBytesReader(w, r.Body, int64(config.Limits.MaxSiteSize.Bytes()))
503
+
result = UpdateFromArchive(ctx, webRoot, contentType, reader)
504
}
505
506
return reportUpdateResult(w, result)
···
554
return nil
555
}
556
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))
569
defer cancel()
570
571
contentType := getMediaType(r.Header.Get("Content-Type"))
572
reader := http.MaxBytesReader(w, r.Body, int64(config.Limits.MaxSiteSize.Bytes()))
573
+
result := PartialUpdateFromArchive(ctx, webRoot, contentType, reader, parents)
574
return reportUpdateResult(w, result)
575
}
576
+19
-5
src/patch.go
+19
-5
src/patch.go
···
12
13
var ErrMalformedPatch = errors.New("malformed patch")
14
15
// Mutates `manifest` according to a tar stream and the following rules:
16
// - A character device with major 0 and minor 0 is a "whiteout marker". When placed
17
// at a given path, this path and its entire subtree (if any) are removed from the manifest.
18
// - When a directory is placed at a given path, this path and its entire subtree (if any) are
19
// removed from the manifest and replaced with the contents of the directory.
20
-
func ApplyTarPatch(manifest *Manifest, reader io.Reader) error {
21
type Node struct {
22
entry *Entry
23
children map[string]*Node
···
72
return fmt.Errorf("%w: %s: not a directory", ErrMalformedPatch, dirName)
73
}
74
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]
79
}
80
}
81
if node.children == nil {
82
dirName := strings.Join(segments[:len(segments)-1], "/")
···
12
13
var ErrMalformedPatch = errors.New("malformed patch")
14
15
+
type CreateParentsMode int
16
+
17
+
const (
18
+
RequireParents CreateParentsMode = iota
19
+
CreateParents
20
+
)
21
+
22
// Mutates `manifest` according to a tar stream and the following rules:
23
// - A character device with major 0 and minor 0 is a "whiteout marker". When placed
24
// at a given path, this path and its entire subtree (if any) are removed from the manifest.
25
// - When a directory is placed at a given path, this path and its entire subtree (if any) are
26
// removed from the manifest and replaced with the contents of the directory.
27
+
func ApplyTarPatch(manifest *Manifest, reader io.Reader, parents CreateParentsMode) error {
28
type Node struct {
29
entry *Entry
30
children map[string]*Node
···
79
return fmt.Errorf("%w: %s: not a directory", ErrMalformedPatch, dirName)
80
}
81
if _, exists := node.children[segment]; !exists {
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
+
}
92
}
93
+
node = node.children[segment]
94
}
95
if node.children == nil {
96
dirName := strings.Join(segments[:len(segments)-1], "/")
+2
-1
src/update.go
+2
-1
src/update.go
···
159
webRoot string,
160
contentType string,
161
reader io.Reader,
162
) (result UpdateResult) {
163
var err error
164
···
177
// `*Manifest` objects, which should never be mutated.
178
newManifest := &Manifest{}
179
proto.Merge(newManifest, oldManifest)
180
-
if err := ApplyTarPatch(newManifest, reader); err != nil {
181
return nil, err
182
} else {
183
return newManifest, nil
···
159
webRoot string,
160
contentType string,
161
reader io.Reader,
162
+
parents CreateParentsMode,
163
) (result UpdateResult) {
164
var err error
165
···
178
// `*Manifest` objects, which should never be mutated.
179
newManifest := &Manifest{}
180
proto.Merge(newManifest, oldManifest)
181
+
if err := ApplyTarPatch(newManifest, reader, parents); err != nil {
182
return nil, err
183
} else {
184
return newManifest, nil