refactor: use nginx -s reload instead of systemd
- doc: add comment
This commit is contained in:
+36
-12
@@ -1,3 +1,5 @@
|
||||
// Package filestore 在一个由 daemon 独占管理的本地根目录中保存不可变文件,
|
||||
// 通过内容哈希校验、硬链接原子提交与符号链接逃逸防护,保证已发布文件与其声明身份严格一致。
|
||||
package filestore
|
||||
|
||||
import (
|
||||
@@ -12,33 +14,41 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ErrDestinationConflict 表示目标位置已存在内容不同的文件,禁止覆盖。
|
||||
var ErrDestinationConflict = errors.New("destination already exists with different content")
|
||||
|
||||
// Identity 是不可变文件在进入事务目录前必须满足的身份。
|
||||
// Identity 不可变文件在进入事务目录前必须满足的身份,由精确大小与 SHA-256 摘要组成。
|
||||
type Identity struct {
|
||||
Size int64
|
||||
// Size 期望的精确字节数,必须为非负。
|
||||
Size int64
|
||||
// SHA256 期望的十六进制 SHA-256 摘要,必须为 64 个十六进制字符。
|
||||
SHA256 string
|
||||
}
|
||||
|
||||
// Validate checks the exact immutable file identity without reading a file.
|
||||
// Validate 校验 Identity 的大小与 SHA-256 格式是否合法,不读取任何文件。
|
||||
func (i Identity) Validate() error {
|
||||
_, err := validateIdentity(i)
|
||||
return err
|
||||
}
|
||||
|
||||
// File 是一次原子提交的结果。
|
||||
// File 一次原子提交的返回结果。
|
||||
type File struct {
|
||||
Path string
|
||||
// Path 最终文件在存储根目录下的绝对路径。
|
||||
Path string
|
||||
// Identity 最终文件满足的精确身份。
|
||||
Identity Identity
|
||||
Reused bool
|
||||
// Reused 为 true 表示目标位置已存在身份相同的文件,本次提交未创建新文件。
|
||||
Reused bool
|
||||
}
|
||||
|
||||
// Store 在一个 daemon 独占管理的本地根目录中保存不可变文件。
|
||||
// Store 在一个 daemon 独占管理的本地根目录中保存不可变文件,并保证所有写入都经过身份校验。
|
||||
type Store struct {
|
||||
// root 解析符号链接后的规范根目录绝对路径,所有目标路径都必须落在其中。
|
||||
root string
|
||||
}
|
||||
|
||||
// New 创建文件存储并固定其规范根目录。
|
||||
// New 创建文件存储并固定其规范根目录。root 必须非空,最终会转换为绝对路径、
|
||||
// 创建目录并解析符号链接,确保后续操作都基于稳定且真实存在的根目录。
|
||||
func New(root string) (*Store, error) {
|
||||
if root == "" {
|
||||
return nil, errors.New("file store root is required")
|
||||
@@ -57,8 +67,10 @@ func New(root string) (*Store, error) {
|
||||
return &Store{root: resolvedRoot}, nil
|
||||
}
|
||||
|
||||
// Commit 把内容写入同目录临时文件,校验后通过硬链接原子创建最终文件。
|
||||
// 最终文件已经存在且身份相同时按幂等成功处理,内容不同时拒绝覆盖。
|
||||
// Commit 把 source 的内容写入目标目录内的临时文件,校验其大小与 SHA-256 与 expected 一致后,
|
||||
// 通过硬链接原子地创建 relativePath 指向的最终文件。若最终文件已存在且身份相同,则按幂等
|
||||
// 成功返回 Reused 为 true 的结果;若身份不同则返回 ErrDestinationConflict,拒绝覆盖。
|
||||
// 写入过程会同步临时文件与父目录,确保中途失败不会留下已发布的半成品文件。
|
||||
func (s *Store) Commit(relativePath string, source io.Reader, expected Identity) (File, error) {
|
||||
if source == nil {
|
||||
return File{}, errors.New("file source is required")
|
||||
@@ -145,8 +157,9 @@ func (s *Store) Commit(relativePath string, source io.Reader, expected Identity)
|
||||
return File{Path: target, Identity: expected}, nil
|
||||
}
|
||||
|
||||
// Inspect verifies an immutable destination without changing it.
|
||||
// found=false means that the exact destination does not exist.
|
||||
// Inspect 在不做任何修改的情况下校验 relativePath 指向的不可变目标是否与 expected 一致。
|
||||
// 返回的 found 为 false 表示该目标尚不存在(或所在目录尚不存在);目标存在但身份不符时
|
||||
// 返回 ErrDestinationConflict。
|
||||
func (s *Store) Inspect(relativePath string, expected Identity) (file File, found bool, err error) {
|
||||
expectedDigest, err := validateIdentity(expected)
|
||||
if err != nil {
|
||||
@@ -168,6 +181,8 @@ func (s *Store) Inspect(relativePath string, expected Identity) (file File, foun
|
||||
return verifyExisting(target, expected, expectedDigest)
|
||||
}
|
||||
|
||||
// destination 将 relativePath 规范化后拼接到存储根目录,返回最终绝对路径。
|
||||
// 它拒绝空路径、非本地相对路径(如 ../、绝对路径或含 .. 的越界路径)。
|
||||
func (s *Store) destination(relativePath string) (string, error) {
|
||||
if !filepath.IsLocal(relativePath) || relativePath == "." {
|
||||
return "", fmt.Errorf("file store path is not a local relative path: %q", relativePath)
|
||||
@@ -175,6 +190,8 @@ func (s *Store) destination(relativePath string) (string, error) {
|
||||
return filepath.Join(s.root, filepath.Clean(relativePath)), nil
|
||||
}
|
||||
|
||||
// verifyParent 解析 parent 的符号链接后,确认其仍位于存储根目录之内,
|
||||
// 防止通过符号链接把文件写出根目录之外。
|
||||
func (s *Store) verifyParent(parent string) error {
|
||||
resolvedParent, err := filepath.EvalSymlinks(parent)
|
||||
if err != nil {
|
||||
@@ -190,6 +207,8 @@ func (s *Store) verifyParent(parent string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateIdentity 校验 identity 的大小非负、SHA-256 为合法 64 位十六进制字符串,
|
||||
// 并返回解码后的摘要字节切片供后续比较使用。
|
||||
func validateIdentity(identity Identity) ([]byte, error) {
|
||||
if identity.Size < 0 {
|
||||
return nil, errors.New("expected file size must not be negative")
|
||||
@@ -201,6 +220,9 @@ func validateIdentity(identity Identity) ([]byte, error) {
|
||||
return digest, nil
|
||||
}
|
||||
|
||||
// verifyExisting 检查 path 处是否已存在目标文件:不存在时返回 found=false;
|
||||
// 存在但不是普通文件(或为符号链接)、大小不符、摘要不符时分别返回错误或
|
||||
// ErrDestinationConflict;完全一致时返回 Reused 为 true 的 File。
|
||||
func verifyExisting(path string, expected Identity, expectedDigest []byte) (File, bool, error) {
|
||||
info, err := os.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
@@ -231,10 +253,12 @@ func verifyExisting(path string, expected Identity, expectedDigest []byte) (File
|
||||
return File{Path: path, Identity: expected, Reused: true}, true, nil
|
||||
}
|
||||
|
||||
// sameDigest 使用常数时间比较判断实际摘要与期望摘要是否一致,避免时序侧信道。
|
||||
func sameDigest(actual hash.Hash, expected []byte) bool {
|
||||
return subtle.ConstantTimeCompare(actual.Sum(nil), expected) == 1
|
||||
}
|
||||
|
||||
// syncDirectory 打开 path 指向的目录并调用 Sync 将其刷入磁盘,保证目录项变更持久化。
|
||||
func syncDirectory(path string) error {
|
||||
directory, err := os.Open(path)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user