3 Commits

Author SHA1 Message Date
s1d3sw1ped bdbe1a9416 Update AGENTS.md and Makefile for improved build and test instructions
CI / Go Tests (push) Successful in 12s
CI / Build (push) Successful in 9s
Format / gofmt (push) Successful in 6s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 15s
Release Artifacts / Build and release Docker image (push) Successful in 26s
- Revised the build and test instructions in `AGENTS.md` to emphasize the importance of formatting, testing, and building before finalizing code changes.
- Enhanced comments in the `Makefile` to clarify the purpose of each target, including `run`, `dev`, `build`, `docker`, `test`, `test-race`, and `fmt`.
2026-06-01 05:58:22 -05:00
s1d3sw1ped e6801007ee Refactor constants in filesystem.go for improved readability
- Adjusted formatting of constant declarations in `filesystem.go` to enhance code clarity and maintainability.
2026-06-01 05:58:15 -05:00
s1d3sw1ped a3162a3e5f Enhance scratchbox storage metadata management
CI / Go Tests (push) Successful in 14s
CI / Build (push) Successful in 8s
Format / gofmt (push) Failing after 4s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 14s
Release Artifacts / Build and release Docker image (push) Successful in 24s
- Updated `README.md` to clarify the distinction between public scratch IDs and internal blob IDs.
- Modified `filesystem.go` to introduce a new `BlobID` field in the `Metadata` struct, ensuring the separation of public and internal identifiers.
- Implemented functions to generate and reserve unique public IDs for scratch entries, improving metadata integrity.
- Adjusted tests in `filesystem_test.go` to validate the new ID structure and ensure public IDs do not match blob hashes.
2026-06-01 05:50:37 -05:00
5 changed files with 70 additions and 30 deletions
+1 -4
View File
@@ -29,10 +29,7 @@ Key product constraints:
## Build and test ## Build and test
Use `make help` as the source of truth for available build/test targets and descriptions. Use `make help` as the source of truth for available build/test targets and descriptions.
Always build and test via make unless it doesn't include targets for that. Format, Test, Build are the required steps before being done with any coding if there are more then one target for any of these then run them all before considering that step complete.
Also in the event there is a test without race checking and one with do both.
Otherwise just whats available.
To verify your changes you must run tests and do a build.
## Coding conventions ## Coding conventions
+7 -7
View File
@@ -18,26 +18,26 @@ help:
@echo " make fmt - Format Go code" @echo " make fmt - Format Go code"
@echo " make clean - Remove built artifacts" @echo " make clean - Remove built artifacts"
run: dev run: dev # Run the application alias for dev.
dev: build dev: build # Run the application.
$(BIN) server --config $(CONFIG) $(BIN) server --config $(CONFIG)
build: clean build: clean # Build the application.
npm --prefix ./web/ui run build npm --prefix ./web/ui run build
mkdir -p $(BIN_DIR) mkdir -p $(BIN_DIR)
CGO_ENABLED=0 go build -o $(BIN) $(CMD_DIR) CGO_ENABLED=0 go build -o $(BIN) $(CMD_DIR)
docker: docker: # Build the Docker image.
docker build -t $(DOCKER_IMAGE) . docker build -t $(DOCKER_IMAGE) .
test: test: # Run the tests.
go test -timeout 30s -shuffle=on ./... go test -timeout 30s -shuffle=on ./...
test-race: test-race: # Run the tests with the race detector.
go test -race -timeout 30s -shuffle=on ./... go test -race -timeout 30s -shuffle=on ./...
fmt: fmt: # Format the code using gofmt.
go fmt ./... go fmt ./...
clean: clean:
+1 -1
View File
@@ -294,4 +294,4 @@ Scratchbox storage is designed for a single process instance per `storage.data_d
- Do not run multiple scratchbox processes against the same data directory. - Do not run multiple scratchbox processes against the same data directory.
- Metadata/index writes are process-local and are not coordinated with cross-process locking. - Metadata/index writes are process-local and are not coordinated with cross-process locking.
- Scratch payload files and metadata index are encrypted at rest. - Scratch payload files and metadata index are encrypted at rest.
- Scratch IDs are derived from the SHA-256 of the stored blob (encrypted+gzip payload on disk). - Public scratch IDs are short random aliases; stored blobs still use SHA-256-derived internal blob IDs on disk.
+52 -12
View File
@@ -24,6 +24,9 @@ const (
indexFilename = "metadata" indexFilename = "metadata"
filesDirName = "scratches" filesDirName = "scratches"
encryptedChunkSize = 64 * 1024 encryptedChunkSize = 64 * 1024
publicIDLength = 12
publicIDAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
publicIDMaxAttempts = 8
) )
var ErrNotFound = errors.New("scratch not found") var ErrNotFound = errors.New("scratch not found")
@@ -33,6 +36,7 @@ var encryptedIndexMagic = [4]byte{'S', 'M', 'D', '1'}
type Metadata struct { type Metadata struct {
ID string `json:"id"` ID string `json:"id"`
BlobID string `json:"blob_id,omitempty"`
FilePath string `json:"file_path"` FilePath string `json:"file_path"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
ExpiresAt time.Time `json:"expires_at"` ExpiresAt time.Time `json:"expires_at"`
@@ -146,8 +150,8 @@ func (s *FilesystemStore) CreateWithOriginalName(ctx context.Context, body io.Re
return Metadata{}, fmt.Errorf("close temp file: %w", closeErr) return Metadata{}, fmt.Errorf("close temp file: %w", closeErr)
} }
id := hex.EncodeToString(blobHash.Sum(nil)) blobID := hex.EncodeToString(blobHash.Sum(nil))
finalPath := filepath.Join(s.filesDir, id) finalPath := filepath.Join(s.filesDir, blobID)
if _, statErr := os.Stat(finalPath); statErr == nil { if _, statErr := os.Stat(finalPath); statErr == nil {
_ = os.Remove(tempFile.Name()) _ = os.Remove(tempFile.Name())
return Metadata{}, errors.New("scratch id collision for content hash") return Metadata{}, errors.New("scratch id collision for content hash")
@@ -161,8 +165,17 @@ func (s *FilesystemStore) CreateWithOriginalName(ctx context.Context, body io.Re
} }
now := time.Now().UTC() now := time.Now().UTC()
s.mu.Lock()
publicID, err := reservePublicID(s.metadata)
if err != nil {
s.mu.Unlock()
_ = os.Remove(finalPath)
return Metadata{}, err
}
meta := Metadata{ meta := Metadata{
ID: id, ID: publicID,
BlobID: blobID,
FilePath: finalPath, FilePath: finalPath,
CreatedAt: now, CreatedAt: now,
ExpiresAt: now.Add(ttl), ExpiresAt: now.Add(ttl),
@@ -170,16 +183,9 @@ func (s *FilesystemStore) CreateWithOriginalName(ctx context.Context, body io.Re
OriginalName: normalizeOriginalName(originalName), OriginalName: normalizeOriginalName(originalName),
Size: size, Size: size,
} }
s.metadata[publicID] = meta
s.mu.Lock()
if _, exists := s.metadata[id]; exists {
s.mu.Unlock()
_ = os.Remove(finalPath)
return Metadata{}, errors.New("scratch id collision in metadata index")
}
s.metadata[id] = meta
if err := s.persistLocked(); err != nil { if err := s.persistLocked(); err != nil {
delete(s.metadata, id) delete(s.metadata, publicID)
s.mu.Unlock() s.mu.Unlock()
_ = os.Remove(finalPath) _ = os.Remove(finalPath)
return Metadata{}, err return Metadata{}, err
@@ -314,6 +320,15 @@ func (s *FilesystemStore) loadIndex() error {
if doc.Scratches == nil { if doc.Scratches == nil {
doc.Scratches = map[string]Metadata{} doc.Scratches = map[string]Metadata{}
} }
for key, meta := range doc.Scratches {
if strings.TrimSpace(meta.ID) == "" {
meta.ID = key
}
if strings.TrimSpace(meta.BlobID) == "" {
meta.BlobID = filepath.Base(meta.FilePath)
}
doc.Scratches[key] = meta
}
s.metadata = doc.Scratches s.metadata = doc.Scratches
if ttlRaw := strings.TrimSpace(doc.DefaultTTL); ttlRaw != "" { if ttlRaw := strings.TrimSpace(doc.DefaultTTL); ttlRaw != "" {
ttl, parseErr := time.ParseDuration(ttlRaw) ttl, parseErr := time.ParseDuration(ttlRaw)
@@ -582,6 +597,31 @@ func equalBytes(a, b []byte) bool {
return true return true
} }
func reservePublicID(existing map[string]Metadata) (string, error) {
for i := 0; i < publicIDMaxAttempts; i++ {
id, err := generatePublicID()
if err != nil {
return "", err
}
if _, exists := existing[id]; !exists {
return id, nil
}
}
return "", errors.New("unable to reserve unique public id")
}
func generatePublicID() (string, error) {
buf := make([]byte, publicIDLength)
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("generate public id bytes: %w", err)
}
alphabetLen := byte(len(publicIDAlphabet))
for i := range buf {
buf[i] = publicIDAlphabet[int(buf[i]%alphabetLen)]
}
return string(buf), nil
}
func encryptIndexPayload(plaintext []byte, aead cipher.AEAD) ([]byte, error) { func encryptIndexPayload(plaintext []byte, aead cipher.AEAD) ([]byte, error) {
nonce := make([]byte, aead.NonceSize()) nonce := make([]byte, aead.NonceSize())
if _, err := rand.Read(nonce); err != nil { if _, err := rand.Read(nonce); err != nil {
+6 -3
View File
@@ -123,9 +123,12 @@ func TestCreateEncryptedStoreDoesNotExposeGzipHeaderOnDisk(t *testing.T) {
t.Fatalf("ReadFile(encrypted) error = %v", err) t.Fatalf("ReadFile(encrypted) error = %v", err)
} }
sum := sha256.Sum256(filePayload) sum := sha256.Sum256(filePayload)
wantID := hex.EncodeToString(sum[:]) wantBlobID := hex.EncodeToString(sum[:])
if meta.ID != wantID { if meta.BlobID != wantBlobID {
t.Fatalf("meta.ID = %q, want SHA-256 of encrypted blob %q", meta.ID, wantID) t.Fatalf("meta.BlobID = %q, want SHA-256 of encrypted blob %q", meta.BlobID, wantBlobID)
}
if meta.ID == wantBlobID {
t.Fatalf("meta.ID should be a short public id, got blob hash id %q", meta.ID)
} }
} }