tenseleyflow/shithub / 873c0e5

Browse files

Add ObjectStore interface, sentinel errors, and Quota type

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
873c0e5eff5cedc35cf12df9815c542138091dd1
Parents
17c5f7b
Tree
297c32c

3 changed files

StatusFile+-
A internal/infra/storage/errors.go 26 0
A internal/infra/storage/objectstore.go 95 0
A internal/infra/storage/quota.go 33 0
internal/infra/storage/errors.goadded
@@ -0,0 +1,26 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package storage
4
+
5
+import "errors"
6
+
7
+var (
8
+	// ErrNotFound is returned by Get/Stat for absent keys and by reposfs
9
+	// helpers when a path doesn't exist.
10
+	ErrNotFound = errors.New("storage: not found")
11
+
12
+	// ErrPreconditionFailed is returned by Put when an If-None-Match check fails.
13
+	ErrPreconditionFailed = errors.New("storage: precondition failed")
14
+
15
+	// ErrInvalidPath is returned by RepoPath (and any helper that takes
16
+	// owner/repo names) for inputs that violate the whitelist.
17
+	ErrInvalidPath = errors.New("storage: invalid path")
18
+
19
+	// ErrAlreadyExists is returned by Move and InitBare when the destination
20
+	// is already populated. Lets callers distinguish a race from corruption.
21
+	ErrAlreadyExists = errors.New("storage: already exists")
22
+
23
+	// ErrEscapesRoot is returned by Delete and Move when a path resolves
24
+	// outside the configured storage root. Hard fail — never silently ignore.
25
+	ErrEscapesRoot = errors.New("storage: path escapes root")
26
+)
internal/infra/storage/objectstore.goadded
@@ -0,0 +1,95 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+// Package storage provides shithub's storage abstractions:
4
+//   - ObjectStore: a small interface for S3-compatible object storage
5
+//     (works against MinIO in dev/test and DigitalOcean Spaces in prod).
6
+//   - reposfs: filesystem-backed bare git repository helpers (sharded
7
+//     layout, atomic helpers, strict path validation).
8
+//   - Quota: a placeholder type for disk-quota plumbing (no enforcement
9
+//     yet — wired up in later sprints).
10
+//
11
+// Path validation is the security boundary: every entry point that takes
12
+// owner/repo names from outside the package goes through RepoPath, which
13
+// rejects unsafe inputs against a strict whitelist.
14
+package storage
15
+
16
+import (
17
+	"context"
18
+	"io"
19
+	"time"
20
+)
21
+
22
+// ObjectStore is the abstract interface every storage backend implements.
23
+// Implementations: s3 (any S3-compatible endpoint via minio-go) and memory
24
+// (in-process map for tests).
25
+type ObjectStore interface {
26
+	// Put writes body to key. Returns the resulting object's metadata
27
+	// (etag, size). Honors opts.IfNoneMatch (when "*", fails with
28
+	// ErrPreconditionFailed if the key already exists).
29
+	Put(ctx context.Context, key string, body io.Reader, opts PutOpts) (PutResult, error)
30
+
31
+	// Get returns a reader for the object at key. Caller must Close.
32
+	// Returns ErrNotFound when key is absent.
33
+	Get(ctx context.Context, key string) (io.ReadCloser, ObjectMeta, error)
34
+
35
+	// Stat returns metadata for key without fetching the body.
36
+	// Returns ErrNotFound when key is absent.
37
+	Stat(ctx context.Context, key string) (ObjectMeta, error)
38
+
39
+	// Delete removes key. Returns nil for a missing key (idempotent).
40
+	Delete(ctx context.Context, key string) error
41
+
42
+	// List enumerates objects under prefix. Pagination via opts.ContinuationToken.
43
+	List(ctx context.Context, prefix string, opts ListOpts) (ListResult, error)
44
+
45
+	// SignedURL returns a pre-signed URL for the given method ("GET" or
46
+	// "PUT") on key, valid for ttl. The URL grants direct browser/client
47
+	// access without exposing credentials — used for avatar/attachment
48
+	// uploads and large downloads in later sprints.
49
+	SignedURL(ctx context.Context, key string, ttl time.Duration, method string) (string, error)
50
+}
51
+
52
+// PutOpts controls a Put.
53
+type PutOpts struct {
54
+	ContentType string
55
+	// IfNoneMatch, when "*", causes Put to fail with ErrPreconditionFailed
56
+	// if the destination already exists. Other values are not supported.
57
+	IfNoneMatch string
58
+	// ContentLength, when > 0, is passed to the backend as a hint. When 0,
59
+	// the backend buffers / streams as needed.
60
+	ContentLength int64
61
+}
62
+
63
+// PutResult is what Put returns.
64
+type PutResult struct {
65
+	ETag string
66
+	Size int64
67
+}
68
+
69
+// ObjectMeta is the metadata returned by Get/Stat.
70
+type ObjectMeta struct {
71
+	Key          string
72
+	Size         int64
73
+	ETag         string
74
+	ContentType  string
75
+	LastModified time.Time
76
+}
77
+
78
+// ListOpts controls a List.
79
+type ListOpts struct {
80
+	// ContinuationToken resumes pagination from a prior page.
81
+	ContinuationToken string
82
+	// MaxKeys caps the page size. Zero means backend default.
83
+	MaxKeys int
84
+	// Recursive, when false, treats "/" as a delimiter and surfaces common
85
+	// prefixes (folders) in ListResult.CommonPrefixes.
86
+	Recursive bool
87
+}
88
+
89
+// ListResult is one page of a List.
90
+type ListResult struct {
91
+	Objects               []ObjectMeta
92
+	CommonPrefixes        []string
93
+	NextContinuationToken string
94
+	IsTruncated           bool
95
+}
internal/infra/storage/quota.goadded
@@ -0,0 +1,33 @@
1
+// SPDX-License-Identifier: AGPL-3.0-or-later
2
+
3
+package storage
4
+
5
+// Quota is the disk-usage budget for a user or org. Recorded in the DB
6
+// under users.disk_quota_* and orgs.disk_quota_* (added when those tables
7
+// exist). S04 wires the type only — enforcement lives in a future policy
8
+// package called from the push pipeline (S14) and attachment uploads.
9
+type Quota struct {
10
+	Used  int64 // bytes currently used
11
+	Limit int64 // bytes allowed (0 = unlimited)
12
+}
13
+
14
+// Available returns Limit - Used, clamped at zero. Returns -1 when the
15
+// quota is unlimited.
16
+func (q Quota) Available() int64 {
17
+	if q.Limit == 0 {
18
+		return -1
19
+	}
20
+	if q.Used >= q.Limit {
21
+		return 0
22
+	}
23
+	return q.Limit - q.Used
24
+}
25
+
26
+// WouldExceed reports whether writing additional bytes n would push past
27
+// the limit. Always false for an unlimited quota.
28
+func (q Quota) WouldExceed(n int64) bool {
29
+	if q.Limit == 0 {
30
+		return false
31
+	}
32
+	return q.Used+n > q.Limit
33
+}