diff --git a/buckets/meta.go b/buckets/meta.go index 7e8955a..026c451 100644 --- a/buckets/meta.go +++ b/buckets/meta.go @@ -16,7 +16,7 @@ import ( type CreateMetaRequest struct { Name string `json:"name"` Bucket string `json:"bucket"` - FileID string `json:"fileId"` + FileID *string `json:"fileId,omitempty"` EncryptVersion string `json:"encryptVersion"` FolderUuid string `json:"folderUuid"` Size int64 `json:"size"` @@ -40,7 +40,7 @@ type CreateMetaResponse struct { Created string `json:"created"` } -func CreateMetaFile(ctx context.Context, cfg *config.Config, name, bucketID, fileID, encryptVersion, folderUuid, plainName, fileType string, size int64, modTime time.Time) (*CreateMetaResponse, error) { +func CreateMetaFile(ctx context.Context, cfg *config.Config, name, bucketID string, fileID *string, encryptVersion, folderUuid, plainName, fileType string, size int64, modTime time.Time) (*CreateMetaResponse, error) { url := cfg.Endpoints.Drive().Files().Create() reqBody := CreateMetaRequest{ Name: name, diff --git a/buckets/meta_test.go b/buckets/meta_test.go index c6666b4..e60a506 100644 --- a/buckets/meta_test.go +++ b/buckets/meta_test.go @@ -13,6 +13,7 @@ import ( ) func TestCreateMetaFile(t *testing.T) { + fileID := TestFileID testCases := []struct { name string request CreateMetaRequest @@ -26,7 +27,7 @@ func TestCreateMetaFile(t *testing.T) { request: CreateMetaRequest{ Name: TestFileNameNoExt, Bucket: TestBucket1, - FileID: TestFileID, + FileID: &fileID, EncryptVersion: "03-aes", FolderUuid: TestFolderUUID, Size: 1024, @@ -55,7 +56,7 @@ func TestCreateMetaFile(t *testing.T) { request: CreateMetaRequest{ Name: TestFileNameNoExt, Bucket: TestBucket1, - FileID: TestFileID, + FileID: &fileID, EncryptVersion: "03-aes", FolderUuid: TestFolderUUID, Size: 1024, @@ -74,7 +75,7 @@ func TestCreateMetaFile(t *testing.T) { request: CreateMetaRequest{ Name: TestFileNameNoExt, Bucket: TestBucket1, - FileID: TestFileID, + FileID: &fileID, EncryptVersion: "03-aes", FolderUuid: TestFolderUUID, Size: 1024, @@ -177,12 +178,13 @@ func TestCreateMetaFileInvalidJSON(t *testing.T) { cfg := newTestConfig(mockServer.URL) + fileID := TestFileID2 _, err := CreateMetaFile( context.Background(), cfg, TestFileNameNoExt, TestBucket1, - TestFileID2, + &fileID, "03-aes", TestFolderUUID, TestFileNameNoExt, diff --git a/buckets/upload.go b/buckets/upload.go index 1b0975b..c9ba798 100644 --- a/buckets/upload.go +++ b/buckets/upload.go @@ -119,7 +119,7 @@ func UploadFile(ctx context.Context, cfg *config.Config, filePath, targetFolderU base := filepath.Base(filePath) name := strings.TrimSuffix(base, filepath.Ext(base)) ext := strings.TrimPrefix(filepath.Ext(base), ".") - meta, err := CreateMetaFile(ctx, cfg, name, cfg.Bucket, fileID, "03-aes", targetFolderUUID, name, ext, plainSize, modTime) + meta, err := CreateMetaFile(ctx, cfg, name, cfg.Bucket, &fileID, "03-aes", targetFolderUUID, name, ext, plainSize, modTime) if err != nil { return nil, fmt.Errorf("failed to create file metadata: %w", err) } @@ -218,7 +218,7 @@ func UploadFileStream(ctx context.Context, cfg *config.Config, targetFolderUUID, base := filepath.Base(fileName) name := strings.TrimSuffix(base, filepath.Ext(base)) ext := strings.TrimPrefix(filepath.Ext(base), ".") - meta, err := CreateMetaFile(ctx, cfg, name, cfg.Bucket, finishResp.ID, "03-aes", targetFolderUUID, name, ext, plainSize, modTime) + meta, err := CreateMetaFile(ctx, cfg, name, cfg.Bucket, &finishResp.ID, "03-aes", targetFolderUUID, name, ext, plainSize, modTime) if err != nil { return nil, fmt.Errorf("failed to create file metadata: %w", err) } @@ -246,7 +246,7 @@ func UploadFileStreamMultipart(ctx context.Context, cfg *config.Config, targetFo base := filepath.Base(fileName) name := strings.TrimSuffix(base, filepath.Ext(base)) ext := strings.TrimPrefix(filepath.Ext(base), ".") - meta, err := CreateMetaFile(ctx, cfg, name, cfg.Bucket, finishResp.ID, "03-aes", targetFolderUUID, name, ext, plainSize, modTime) + meta, err := CreateMetaFile(ctx, cfg, name, cfg.Bucket, &finishResp.ID, "03-aes", targetFolderUUID, name, ext, plainSize, modTime) if err != nil { return nil, fmt.Errorf("failed to create file metadata: %w", err) } @@ -276,6 +276,17 @@ func UploadFileStreamAuto(ctx context.Context, cfg *config.Config, targetFolderU in = bytes.NewReader(bufferedData) } + if plainSize == 0 { + base := filepath.Base(fileName) + name := strings.TrimSuffix(base, filepath.Ext(base)) + ext := strings.TrimPrefix(filepath.Ext(base), ".") + meta, err := CreateMetaFile(ctx, cfg, name, cfg.Bucket, nil, "03-aes", targetFolderUUID, name, ext, 0, modTime) + if err != nil { + return nil, fmt.Errorf("failed to create empty file metadata: %w", err) + } + return meta, nil + } + var capturedData *bytes.Buffer var capturedReader io.Reader = in diff --git a/buckets/upload_test.go b/buckets/upload_test.go index 7823085..e61374e 100644 --- a/buckets/upload_test.go +++ b/buckets/upload_test.go @@ -1234,3 +1234,234 @@ func TestWaitForPendingThumbnails(t *testing.T) { } }) } + +// TestUploadFileStreamAuto_EmptyFile tests that empty files skip S3 upload and only create metadata +func TestUploadFileStreamAuto_EmptyFile(t *testing.T) { + mockServer := newMockMultiEndpointServer() + defer mockServer.Close() + + startCalled := false + transferCalled := false + finishCalled := false + + mockServer.startHandler = func(w http.ResponseWriter, r *http.Request) { + startCalled = true + t.Error("START handler should not be called for empty files") + w.WriteHeader(http.StatusBadRequest) + } + + mockServer.transferHandler = func(w http.ResponseWriter, r *http.Request) { + transferCalled = true + t.Error("TRANSFER handler should not be called for empty files") + w.WriteHeader(http.StatusBadRequest) + } + + mockServer.finishHandler = func(w http.ResponseWriter, r *http.Request) { + finishCalled = true + t.Error("FINISH handler should not be called for empty files") + w.WriteHeader(http.StatusBadRequest) + } + + metaCalled := false + var capturedFileID *string + var capturedSize int64 + + mockServer.createMetaHandler = func(w http.ResponseWriter, r *http.Request) { + metaCalled = true + var req CreateMetaRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode CreateMetaRequest: %v", err) + } + + capturedFileID = req.FileID + capturedSize = req.Size + + resp := CreateMetaResponse{ + UUID: "empty-file-uuid", + FileID: "", + Name: "empty", + Type: "txt", + Size: "0", + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) + } + + cfg := newTestConfigWithSetup(mockServer.URL(), func(c *config.Config) { + c.Bucket = "test-bucket-empty" + }) + + emptyReader := bytes.NewReader([]byte{}) + result, err := UploadFileStreamAuto(context.Background(), cfg, TestFolderUUID, "empty.txt", emptyReader, 0, time.Now()) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected result, got nil") + } + + if !metaCalled { + t.Error("CreateMetaFile handler should have been called") + } + if startCalled { + t.Error("START handler was called but should have been skipped") + } + if transferCalled { + t.Error("TRANSFER handler was called but should have been skipped") + } + if finishCalled { + t.Error("FINISH handler was called but should have been skipped") + } + + if capturedFileID != nil { + t.Errorf("expected fileID to be nil, got %v", *capturedFileID) + } + + if capturedSize != 0 { + t.Errorf("expected size 0, got %d", capturedSize) + } +} + +// TestUploadFileStreamAuto_EmptyFile_UnknownSize tests empty file upload when size is unknown initially +func TestUploadFileStreamAuto_EmptyFile_UnknownSize(t *testing.T) { + mockServer := newMockMultiEndpointServer() + defer mockServer.Close() + + uploadsCalled := false + + mockServer.startHandler = func(w http.ResponseWriter, r *http.Request) { + uploadsCalled = true + t.Error("Upload handlers should not be called for empty files") + w.WriteHeader(http.StatusBadRequest) + } + + mockServer.transferHandler = func(w http.ResponseWriter, r *http.Request) { + uploadsCalled = true + t.Error("Upload handlers should not be called for empty files") + w.WriteHeader(http.StatusBadRequest) + } + + mockServer.finishHandler = func(w http.ResponseWriter, r *http.Request) { + uploadsCalled = true + t.Error("Upload handlers should not be called for empty files") + w.WriteHeader(http.StatusBadRequest) + } + + metaCalled := false + mockServer.createMetaHandler = func(w http.ResponseWriter, r *http.Request) { + metaCalled = true + var req CreateMetaRequest + json.NewDecoder(r.Body).Decode(&req) + + if req.FileID != nil { + t.Errorf("expected fileID to be nil for empty file, got %v", *req.FileID) + } + if req.Size != 0 { + t.Errorf("expected size 0, got %d", req.Size) + } + + resp := CreateMetaResponse{ + UUID: "empty-uuid", + Size: "0", + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) + } + + cfg := newTestConfigWithSetup(mockServer.URL(), func(c *config.Config) { + c.Bucket = "test-bucket-empty-unknown" + }) + + emptyReader := bytes.NewReader([]byte{}) + result, err := UploadFileStreamAuto(context.Background(), cfg, TestFolderUUID, "empty-unknown.txt", emptyReader, -1, time.Now()) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected result, got nil") + } + + if !metaCalled { + t.Error("CreateMetaFile should have been called") + } + if uploadsCalled { + t.Error("Upload handlers should have been skipped for empty file") + } +} + +// TestUploadFileStream_EmptyFile tests empty file handling with various filenames +func TestUploadFileStream_EmptyFile_ViaStreamAuto(t *testing.T) { + mockServer := newMockMultiEndpointServer() + defer mockServer.Close() + + var capturedRequest *CreateMetaRequest + + mockServer.createMetaHandler = func(w http.ResponseWriter, r *http.Request) { + var req CreateMetaRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode request: %v", err) + } + capturedRequest = &req + + resp := CreateMetaResponse{ + UUID: "test-uuid", + Name: req.Name, + Type: req.Type, + Size: json.Number(fmt.Sprintf("%d", req.Size)), + FileID: "", + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(resp) + } + + cfg := newTestConfigWithSetup(mockServer.URL(), func(c *config.Config) { + c.Bucket = "test-bucket-stream-empty" + }) + + testCases := []struct { + name string + fileName string + expectedName string + expectedType string + }{ + {"simple empty file", "empty.txt", "empty", "txt"}, + {"empty file no extension", "emptyfile", "emptyfile", ""}, + {"empty hidden file", ".hidden", "", "hidden"}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + capturedRequest = nil + + emptyReader := bytes.NewReader([]byte{}) + result, err := UploadFileStreamAuto(context.Background(), cfg, TestFolderUUID, tc.fileName, emptyReader, 0, time.Now()) + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result == nil { + t.Fatal("expected result, got nil") + } + + if capturedRequest == nil { + t.Fatal("CreateMetaFile was not called") + } + + if capturedRequest.PlainName != tc.expectedName { + t.Errorf("expected name '%s', got '%s'", tc.expectedName, capturedRequest.PlainName) + } + if capturedRequest.Type != tc.expectedType { + t.Errorf("expected type '%s', got '%s'", tc.expectedType, capturedRequest.Type) + } + + if capturedRequest.FileID != nil { + t.Errorf("expected fileID to be nil, got %v", *capturedRequest.FileID) + } + if capturedRequest.Size != 0 { + t.Errorf("expected size 0, got %d", capturedRequest.Size) + } + }) + } +}