Changeset 0.22.8 (#150)
This commit is contained in:
@@ -36,7 +36,7 @@ func NewPVC(basePath string) (*PVCStore, error) {
|
||||
}
|
||||
|
||||
// Create well-known subdirectories
|
||||
for _, sub := range []string{"attachments", "processing"} {
|
||||
for _, sub := range []string{"files", "processing"} {
|
||||
if err := os.MkdirAll(filepath.Join(basePath, sub), 0o755); err != nil {
|
||||
return nil, fmt.Errorf("storage/pvc: cannot create %s dir: %w", sub, err)
|
||||
}
|
||||
@@ -236,9 +236,9 @@ func (s *PVCStore) Stats(ctx context.Context) (*StorageStats, error) {
|
||||
}
|
||||
stats.Healthy = true
|
||||
|
||||
// Walk attachments directory for counts
|
||||
attDir := filepath.Join(s.basePath, "attachments")
|
||||
err := filepath.Walk(attDir, func(path string, info os.FileInfo, err error) error {
|
||||
// Walk files directory for counts
|
||||
fileDir := filepath.Join(s.basePath, "files")
|
||||
err := filepath.Walk(fileDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil // skip errors (e.g. permission denied)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestPVC_PutGetRoundTrip(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
data := []byte("hello, storage world")
|
||||
key := "attachments/channel-1/att-1_test.txt"
|
||||
key := "files/channel-1/att-1_test.txt"
|
||||
|
||||
// Put
|
||||
err := s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "text/plain")
|
||||
@@ -56,7 +56,7 @@ func TestPVC_PutAtomicWrite(t *testing.T) {
|
||||
s := tempStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
key := "attachments/ch/file.bin"
|
||||
key := "files/ch/file.bin"
|
||||
data := []byte("atomic test data")
|
||||
|
||||
err := s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "application/octet-stream")
|
||||
@@ -65,7 +65,7 @@ func TestPVC_PutAtomicWrite(t *testing.T) {
|
||||
}
|
||||
|
||||
// File should exist at final path, no temp files left
|
||||
absPath := filepath.Join(s.basePath, "attachments", "ch")
|
||||
absPath := filepath.Join(s.basePath, "files", "ch")
|
||||
entries, _ := os.ReadDir(absPath)
|
||||
for _, e := range entries {
|
||||
if e.Name() != "file.bin" {
|
||||
@@ -79,7 +79,7 @@ func TestPVC_PutSizeMismatch(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
data := []byte("short")
|
||||
err := s.Put(ctx, "attachments/ch/f.txt", bytes.NewReader(data), 999, "text/plain")
|
||||
err := s.Put(ctx, "files/ch/f.txt", bytes.NewReader(data), 999, "text/plain")
|
||||
if err == nil {
|
||||
t.Fatal("expected size mismatch error")
|
||||
}
|
||||
@@ -89,7 +89,7 @@ func TestPVC_GetNotFound(t *testing.T) {
|
||||
s := tempStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, _, _, err := s.Get(ctx, "attachments/nonexistent/file.txt")
|
||||
_, _, _, err := s.Get(ctx, "files/nonexistent/file.txt")
|
||||
if err != ErrNotFound {
|
||||
t.Errorf("err = %v, want ErrNotFound", err)
|
||||
}
|
||||
@@ -99,7 +99,7 @@ func TestPVC_Delete(t *testing.T) {
|
||||
s := tempStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
key := "attachments/ch/delete-me.txt"
|
||||
key := "files/ch/delete-me.txt"
|
||||
_ = s.Put(ctx, key, bytes.NewReader([]byte("bye")), 3, "text/plain")
|
||||
|
||||
err := s.Delete(ctx, key)
|
||||
@@ -118,7 +118,7 @@ func TestPVC_DeleteIdempotent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Delete a file that doesn't exist — should not error
|
||||
err := s.Delete(ctx, "attachments/no-such/file.txt")
|
||||
err := s.Delete(ctx, "files/no-such/file.txt")
|
||||
if err != nil {
|
||||
t.Errorf("Delete nonexistent: %v", err)
|
||||
}
|
||||
@@ -129,18 +129,18 @@ func TestPVC_DeletePrefix(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create several files under a channel prefix
|
||||
prefix := "attachments/channel-abc/"
|
||||
prefix := "files/channel-abc/"
|
||||
for _, name := range []string{"a.txt", "b.png", "c.pdf"} {
|
||||
key := prefix + name
|
||||
_ = s.Put(ctx, key, bytes.NewReader([]byte("x")), 1, "text/plain")
|
||||
}
|
||||
|
||||
// Also create a file in a different channel
|
||||
other := "attachments/channel-other/keep.txt"
|
||||
other := "files/channel-other/keep.txt"
|
||||
_ = s.Put(ctx, other, bytes.NewReader([]byte("y")), 1, "text/plain")
|
||||
|
||||
// Delete the channel prefix
|
||||
err := s.DeletePrefix(ctx, "attachments/channel-abc")
|
||||
err := s.DeletePrefix(ctx, "files/channel-abc")
|
||||
if err != nil {
|
||||
t.Fatalf("DeletePrefix: %v", err)
|
||||
}
|
||||
@@ -165,7 +165,7 @@ func TestPVC_DeletePrefixNonexistent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Should not error
|
||||
err := s.DeletePrefix(ctx, "attachments/does-not-exist/")
|
||||
err := s.DeletePrefix(ctx, "files/does-not-exist/")
|
||||
if err != nil {
|
||||
t.Errorf("DeletePrefix nonexistent: %v", err)
|
||||
}
|
||||
@@ -175,7 +175,7 @@ func TestPVC_Exists(t *testing.T) {
|
||||
s := tempStore(t)
|
||||
ctx := context.Background()
|
||||
|
||||
key := "attachments/ch/exists.txt"
|
||||
key := "files/ch/exists.txt"
|
||||
_ = s.Put(ctx, key, bytes.NewReader([]byte("hi")), 2, "text/plain")
|
||||
|
||||
exists, err := s.Exists(ctx, key)
|
||||
@@ -186,7 +186,7 @@ func TestPVC_Exists(t *testing.T) {
|
||||
t.Error("Exists returned false for existing file")
|
||||
}
|
||||
|
||||
exists, err = s.Exists(ctx, "attachments/ch/nope.txt")
|
||||
exists, err = s.Exists(ctx, "files/ch/nope.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Exists (missing): %v", err)
|
||||
}
|
||||
@@ -220,7 +220,7 @@ func TestPVC_Stats(t *testing.T) {
|
||||
|
||||
// Add some files
|
||||
for i := 0; i < 3; i++ {
|
||||
key := filepath.Join("attachments", "ch", string(rune('a'+i))+".txt")
|
||||
key := filepath.Join("files", "ch", string(rune('a'+i))+".txt")
|
||||
data := bytes.Repeat([]byte("x"), 100*(i+1))
|
||||
_ = s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "text/plain")
|
||||
}
|
||||
@@ -243,7 +243,7 @@ func TestPVC_PathTraversal(t *testing.T) {
|
||||
|
||||
bad := []string{
|
||||
"../etc/passwd",
|
||||
"attachments/../../etc/shadow",
|
||||
"files/../../etc/shadow",
|
||||
"/absolute/path",
|
||||
"",
|
||||
}
|
||||
@@ -270,7 +270,7 @@ func TestPVC_SubdirCreation(t *testing.T) {
|
||||
s := tempStore(t)
|
||||
|
||||
// Verify well-known subdirs were created
|
||||
for _, sub := range []string{"attachments", "processing"} {
|
||||
for _, sub := range []string{"files", "processing"} {
|
||||
info, err := os.Stat(filepath.Join(s.basePath, sub))
|
||||
if err != nil {
|
||||
t.Errorf("subdir %q not created: %v", sub, err)
|
||||
|
||||
@@ -269,10 +269,10 @@ func (s *S3Store) Stats(ctx context.Context) (*StorageStats, error) {
|
||||
}
|
||||
stats.Healthy = true
|
||||
|
||||
// Count objects under the attachments prefix
|
||||
attPrefix := s.fullKey("attachments/")
|
||||
// Count objects under the files prefix
|
||||
filePrefix := s.fullKey("files/")
|
||||
objectsCh := s.client.ListObjects(ctx, s.bucket, minio.ListObjectsOptions{
|
||||
Prefix: attPrefix,
|
||||
Prefix: filePrefix,
|
||||
Recursive: true,
|
||||
})
|
||||
|
||||
|
||||
@@ -44,9 +44,9 @@ func TestS3_FullKey(t *testing.T) {
|
||||
key string
|
||||
want string
|
||||
}{
|
||||
{"", "attachments/ch/f.txt", "attachments/ch/f.txt"},
|
||||
{"switchboard/", "attachments/ch/f.txt", "switchboard/attachments/ch/f.txt"},
|
||||
{"prefix", "attachments/ch/f.txt", "prefix/attachments/ch/f.txt"}, // trailing slash added by NewS3
|
||||
{"", "files/ch/f.txt", "files/ch/f.txt"},
|
||||
{"switchboard/", "files/ch/f.txt", "switchboard/files/ch/f.txt"},
|
||||
{"prefix", "files/ch/f.txt", "prefix/files/ch/f.txt"}, // trailing slash added by NewS3
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -68,7 +68,7 @@ func TestS3_KeyValidation(t *testing.T) {
|
||||
"",
|
||||
"../etc/passwd",
|
||||
"/absolute/path",
|
||||
"attachments/../../etc/shadow",
|
||||
"files/../../etc/shadow",
|
||||
}
|
||||
for _, key := range bad {
|
||||
if err := validateKey(key); err == nil {
|
||||
@@ -77,8 +77,8 @@ func TestS3_KeyValidation(t *testing.T) {
|
||||
}
|
||||
|
||||
good := []string{
|
||||
"attachments/ch/f.txt",
|
||||
"attachments/channel-1/att-abc_test.pdf",
|
||||
"files/ch/f.txt",
|
||||
"files/channel-1/att-abc_test.pdf",
|
||||
"processing/abc123/status.json",
|
||||
}
|
||||
for _, key := range good {
|
||||
@@ -133,7 +133,7 @@ func testS3Store(t *testing.T) *S3Store {
|
||||
// Clean up prefix after test
|
||||
t.Cleanup(func() {
|
||||
ctx := context.Background()
|
||||
_ = s.DeletePrefix(ctx, "attachments")
|
||||
_ = s.DeletePrefix(ctx, "files")
|
||||
_ = s.DeletePrefix(ctx, "processing")
|
||||
})
|
||||
|
||||
@@ -145,7 +145,7 @@ func TestS3_Integration_PutGetRoundTrip(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
data := []byte("hello, S3 storage world")
|
||||
key := "attachments/channel-1/att-1_test.txt"
|
||||
key := "files/channel-1/att-1_test.txt"
|
||||
|
||||
err := s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "text/plain")
|
||||
if err != nil {
|
||||
@@ -175,7 +175,7 @@ func TestS3_Integration_GetNotFound(t *testing.T) {
|
||||
s := testS3Store(t)
|
||||
ctx := context.Background()
|
||||
|
||||
_, _, _, err := s.Get(ctx, "attachments/nonexistent/file.txt")
|
||||
_, _, _, err := s.Get(ctx, "files/nonexistent/file.txt")
|
||||
if err != ErrNotFound {
|
||||
t.Errorf("err = %v, want ErrNotFound", err)
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func TestS3_Integration_Delete(t *testing.T) {
|
||||
s := testS3Store(t)
|
||||
ctx := context.Background()
|
||||
|
||||
key := "attachments/ch/delete-me.txt"
|
||||
key := "files/ch/delete-me.txt"
|
||||
_ = s.Put(ctx, key, bytes.NewReader([]byte("bye")), 3, "text/plain")
|
||||
|
||||
err := s.Delete(ctx, key)
|
||||
@@ -204,7 +204,7 @@ func TestS3_Integration_DeleteIdempotent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
// S3 delete is inherently idempotent
|
||||
err := s.Delete(ctx, "attachments/no-such/file.txt")
|
||||
err := s.Delete(ctx, "files/no-such/file.txt")
|
||||
if err != nil {
|
||||
t.Errorf("Delete nonexistent: %v", err)
|
||||
}
|
||||
@@ -214,16 +214,16 @@ func TestS3_Integration_DeletePrefix(t *testing.T) {
|
||||
s := testS3Store(t)
|
||||
ctx := context.Background()
|
||||
|
||||
prefix := "attachments/channel-abc/"
|
||||
prefix := "files/channel-abc/"
|
||||
for _, name := range []string{"a.txt", "b.png", "c.pdf"} {
|
||||
key := prefix + name
|
||||
_ = s.Put(ctx, key, bytes.NewReader([]byte("x")), 1, "text/plain")
|
||||
}
|
||||
|
||||
other := "attachments/channel-other/keep.txt"
|
||||
other := "files/channel-other/keep.txt"
|
||||
_ = s.Put(ctx, other, bytes.NewReader([]byte("y")), 1, "text/plain")
|
||||
|
||||
err := s.DeletePrefix(ctx, "attachments/channel-abc")
|
||||
err := s.DeletePrefix(ctx, "files/channel-abc")
|
||||
if err != nil {
|
||||
t.Fatalf("DeletePrefix: %v", err)
|
||||
}
|
||||
@@ -245,7 +245,7 @@ func TestS3_Integration_Exists(t *testing.T) {
|
||||
s := testS3Store(t)
|
||||
ctx := context.Background()
|
||||
|
||||
key := "attachments/ch/exists.txt"
|
||||
key := "files/ch/exists.txt"
|
||||
_ = s.Put(ctx, key, bytes.NewReader([]byte("hi")), 2, "text/plain")
|
||||
|
||||
exists, err := s.Exists(ctx, key)
|
||||
@@ -256,7 +256,7 @@ func TestS3_Integration_Exists(t *testing.T) {
|
||||
t.Error("Exists returned false for existing file")
|
||||
}
|
||||
|
||||
exists, err = s.Exists(ctx, "attachments/ch/nope.txt")
|
||||
exists, err = s.Exists(ctx, "files/ch/nope.txt")
|
||||
if err != nil {
|
||||
t.Fatalf("Exists (missing): %v", err)
|
||||
}
|
||||
@@ -278,7 +278,7 @@ func TestS3_Integration_Stats(t *testing.T) {
|
||||
|
||||
// Add some files
|
||||
for i := 0; i < 3; i++ {
|
||||
key := "attachments/ch/" + string(rune('a'+i)) + ".txt"
|
||||
key := "files/ch/" + string(rune('a'+i)) + ".txt"
|
||||
data := bytes.Repeat([]byte("x"), 100*(i+1))
|
||||
_ = s.Put(ctx, key, bytes.NewReader(data), int64(len(data)), "text/plain")
|
||||
}
|
||||
@@ -306,7 +306,7 @@ func TestS3_Integration_PutUnknownSize(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
data := []byte("unknown size upload")
|
||||
key := "attachments/ch/unknown-size.txt"
|
||||
key := "files/ch/unknown-size.txt"
|
||||
|
||||
// size=0 means unknown — S3 should handle via multipart
|
||||
err := s.Put(ctx, key, bytes.NewReader(data), 0, "text/plain")
|
||||
|
||||
@@ -24,7 +24,7 @@ var (
|
||||
//
|
||||
// Keys are slash-delimited paths relative to the storage root:
|
||||
//
|
||||
// attachments/{channel_id}/{attachment_id}_{filename}
|
||||
// files/{channel_id}/{file_id}_{filename}
|
||||
//
|
||||
// Implementations must create intermediate directories as needed.
|
||||
type ObjectStore interface {
|
||||
@@ -44,7 +44,7 @@ type ObjectStore interface {
|
||||
|
||||
// DeletePrefix removes all objects under the given prefix.
|
||||
// Used for channel deletion (bulk cleanup).
|
||||
// Example: DeletePrefix(ctx, "attachments/{channel_id}/")
|
||||
// Example: DeletePrefix(ctx, "files/{channel_id}/")
|
||||
DeletePrefix(ctx context.Context, prefix string) error
|
||||
|
||||
// Exists checks if an object exists at key without reading it.
|
||||
|
||||
Reference in New Issue
Block a user