~bigbes/sr-ht-dolt

ref: 19645e70751196242d9c3bd4bdd002f43d89ee2a sr-ht-dolt/storage/init.go -rw-r--r-- 4.5 KiB
19645e70 — Eugene Blikh storage: bare store init/delete and remotesrv DBCache 30 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
// Package storage owns dolt.sr.ht's on-disk NBS chunk stores and the
// remotesapi DBCache that serves them.
//
// A hosted database is a bare NBS chunk-store directory (no ".dolt/", no
// working set) laid out at "<root>/~<owner>/<name>". This is exactly what
// remotesrv serves and what "file://" dolt remotes consume, so InitStore can
// create one with the low-level doltdb primitives and remotesrv can read and
// write it directly.
//
// # remotesrv FS working directory (load-bearing)
//
// remotesrv seals its chunk-download URLs relative to the working directory of
// the filesys it is given. The server MUST be constructed with
// filesys.LocalFilesysWithWorkingDir(root) pointing at the repos root — NOT a
// plain filesys.LocalFS. With a working-dir-rooted FS the sealed URLs carry
// clean relative prefixes (e.g. "~owner/db"); with a bare LocalFS they carry
// "../../.." escapes that the sealed-URL file handler rejects, and every clone
// or push breaks at the chunk-transfer stage. This was proven end-to-end in the
// Phase-0 spike (storage/spike_test.go). Cache in this package keys stores by
// absolute disk path, so it works with either FS, but the server assembly in
// remoteapi/ must still honor this rule.
package storage

import (
	"context"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
	"github.com/dolthub/dolt/go/libraries/utils/earl"
	"github.com/dolthub/dolt/go/libraries/utils/filesys"
	"github.com/dolthub/dolt/go/store/types"
)

// RepoDiskPath returns the absolute on-disk store directory for a database,
// laid out as "<root>/~<owner>/<name>". Callers are responsible for validating
// owner and name (see core.ValidateName) before touching disk.
func RepoDiskPath(root, owner, name string) string {
	return filepath.Join(root, "~"+owner, name)
}

// InitStore creates a bare NBS chunk store at absPath and writes an empty repo
// into it with a single "main" branch and an initial commit authored by
// ownerName/ownerEmail.
//
// absPath must be absolute. On any failure after the directory is created,
// InitStore removes absPath so a failed creation never leaves a partial store
// behind. Idempotence is NOT provided: calling InitStore on an existing store
// is a caller error and is not defended against here.
func InitStore(ctx context.Context, absPath, ownerName, ownerEmail string) (err error) {
	if !filepath.IsAbs(absPath) {
		return fmt.Errorf("storage: InitStore requires an absolute path, got %q", absPath)
	}

	if err := os.MkdirAll(absPath, 0o755); err != nil {
		return fmt.Errorf("storage: create store dir %q: %w", absPath, err)
	}
	// Any error past this point must not leave a half-written store behind.
	defer func() {
		if err != nil {
			os.RemoveAll(absPath)
		}
	}()

	fileURL := earl.FileUrlFromPath(absPath, os.PathSeparator)
	ddb, err := doltdb.LoadDoltDB(ctx, types.Format_DOLT, fileURL, filesys.LocalFS)
	if err != nil {
		return fmt.Errorf("storage: load doltdb at %q: %w", fileURL, err)
	}

	if err = ddb.WriteEmptyRepo(ctx, "main", ownerName, ownerEmail); err != nil {
		ddb.Close()
		return fmt.Errorf("storage: write empty repo at %q: %w", absPath, err)
	}

	// Release the init handle so the server can open its own store over the
	// same directory later.
	if err = ddb.Close(); err != nil {
		return fmt.Errorf("storage: close init doltdb at %q: %w", absPath, err)
	}
	return nil
}

// DeleteStore removes the store directory at absPath. It refuses to delete
// anything that is not strictly contained within root, guarding against a
// corrupted or attacker-controlled path escaping the configured repos root.
// Both root and absPath must be absolute.
func DeleteStore(ctx context.Context, root, absPath string) error {
	if !filepath.IsAbs(root) {
		return fmt.Errorf("storage: DeleteStore requires an absolute root, got %q", root)
	}
	if !filepath.IsAbs(absPath) {
		return fmt.Errorf("storage: DeleteStore requires an absolute path, got %q", absPath)
	}

	cleanRoot := filepath.Clean(root)
	cleanPath := filepath.Clean(absPath)
	if cleanPath == cleanRoot {
		return fmt.Errorf("storage: refusing to delete the repos root %q", cleanRoot)
	}
	rel, err := filepath.Rel(cleanRoot, cleanPath)
	if err != nil {
		return fmt.Errorf("storage: DeleteStore rel(%q, %q): %w", cleanRoot, cleanPath, err)
	}
	if rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
		return fmt.Errorf("storage: refusing to delete %q outside repos root %q", cleanPath, cleanRoot)
	}

	if err := os.RemoveAll(cleanPath); err != nil {
		return fmt.Errorf("storage: remove store %q: %w", cleanPath, err)
	}
	return nil
}