|
|
|
@@ -21,9 +21,12 @@ import (
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
indexFilename = "metadata"
|
|
|
|
|
filesDirName = "scratches"
|
|
|
|
|
encryptedChunkSize = 64 * 1024
|
|
|
|
|
indexFilename = "metadata"
|
|
|
|
|
filesDirName = "scratches"
|
|
|
|
|
encryptedChunkSize = 64 * 1024
|
|
|
|
|
publicIDLength = 12
|
|
|
|
|
publicIDAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
|
|
|
publicIDMaxAttempts = 8
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var ErrNotFound = errors.New("scratch not found")
|
|
|
|
@@ -33,6 +36,7 @@ var encryptedIndexMagic = [4]byte{'S', 'M', 'D', '1'}
|
|
|
|
|
|
|
|
|
|
type Metadata struct {
|
|
|
|
|
ID string `json:"id"`
|
|
|
|
|
BlobID string `json:"blob_id,omitempty"`
|
|
|
|
|
FilePath string `json:"file_path"`
|
|
|
|
|
CreatedAt time.Time `json:"created_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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id := hex.EncodeToString(blobHash.Sum(nil))
|
|
|
|
|
finalPath := filepath.Join(s.filesDir, id)
|
|
|
|
|
blobID := hex.EncodeToString(blobHash.Sum(nil))
|
|
|
|
|
finalPath := filepath.Join(s.filesDir, blobID)
|
|
|
|
|
if _, statErr := os.Stat(finalPath); statErr == nil {
|
|
|
|
|
_ = os.Remove(tempFile.Name())
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
s.mu.Lock()
|
|
|
|
|
publicID, err := reservePublicID(s.metadata)
|
|
|
|
|
if err != nil {
|
|
|
|
|
s.mu.Unlock()
|
|
|
|
|
_ = os.Remove(finalPath)
|
|
|
|
|
return Metadata{}, err
|
|
|
|
|
}
|
|
|
|
|
meta := Metadata{
|
|
|
|
|
ID: id,
|
|
|
|
|
ID: publicID,
|
|
|
|
|
BlobID: blobID,
|
|
|
|
|
FilePath: finalPath,
|
|
|
|
|
CreatedAt: now,
|
|
|
|
|
ExpiresAt: now.Add(ttl),
|
|
|
|
@@ -170,16 +183,9 @@ func (s *FilesystemStore) CreateWithOriginalName(ctx context.Context, body io.Re
|
|
|
|
|
OriginalName: normalizeOriginalName(originalName),
|
|
|
|
|
Size: size,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
s.metadata[publicID] = meta
|
|
|
|
|
if err := s.persistLocked(); err != nil {
|
|
|
|
|
delete(s.metadata, id)
|
|
|
|
|
delete(s.metadata, publicID)
|
|
|
|
|
s.mu.Unlock()
|
|
|
|
|
_ = os.Remove(finalPath)
|
|
|
|
|
return Metadata{}, err
|
|
|
|
@@ -314,6 +320,15 @@ func (s *FilesystemStore) loadIndex() error {
|
|
|
|
|
if doc.Scratches == nil {
|
|
|
|
|
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
|
|
|
|
|
if ttlRaw := strings.TrimSpace(doc.DefaultTTL); ttlRaw != "" {
|
|
|
|
|
ttl, parseErr := time.ParseDuration(ttlRaw)
|
|
|
|
@@ -582,6 +597,31 @@ func equalBytes(a, b []byte) bool {
|
|
|
|
|
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) {
|
|
|
|
|
nonce := make([]byte, aead.NonceSize())
|
|
|
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
|
|
|