~bigbes/sr-ht-dolt

ref: 51a5acf935a1f7fbcf879fb9f748331e5a27a559 sr-ht-dolt/storage/init.go -rw-r--r-- 6.3 KiB
51a5acf9 — Eugene Blikh style(web): restyle the beads parade in the todo.sr.ht idiom 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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
// 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/nbs"
	"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
}

// InitEmptyStore creates a genuinely empty bare NBS chunk store at absPath —
// a directory whose store root is the empty hash, with NO commits and NO
// working set. Unlike InitStore it deliberately does NOT call WriteEmptyRepo:
// an initial "Initialize data repository" commit would make the first push to
// this store a non-fast-forward and be rejected. An empty store lets the
// client's first push land as the initial history. Used by push-to-create.
// absPath must be absolute; on any failure the directory is removed.
func InitEmptyStore(ctx context.Context, absPath string) (err error) {
	if !filepath.IsAbs(absPath) {
		return fmt.Errorf("storage: InitEmptyStore 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)
		}
	}()

	// Opening a plain (non-generational) NBS store over the freshly created,
	// empty directory both validates the directory (checkDir requires it to
	// exist) and confirms the store is a valid empty store (a missing manifest
	// is treated lazily as an empty store with the null root). This mirrors the
	// construction storage.Cache.Get uses to serve pushes; a plain store closes
	// cleanly, unlike the generational store LoadDoltDB routes through.
	cs, err := nbs.NewLocalStore(ctx, types.Format_DOLT.VersionString(), absPath, defaultMemTableSize, nbs.NewUnlimitedMemQuotaProvider(), false)
	if err != nil {
		return fmt.Errorf("storage: open empty store at %q: %w", absPath, err)
	}
	if err = cs.Close(); err != nil {
		return fmt.Errorf("storage: close empty store 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
}