Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions buckets/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,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"`
Expand All @@ -39,7 +39,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,
Expand Down
10 changes: 6 additions & 4 deletions buckets/meta_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
)

func TestCreateMetaFile(t *testing.T) {
fileID := TestFileID
testCases := []struct {
name string
request CreateMetaRequest
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
17 changes: 14 additions & 3 deletions buckets/upload.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,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, 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)
}
Expand Down Expand Up @@ -168,7 +168,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)
}
Expand Down Expand Up @@ -196,7 +196,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)
}
Expand Down Expand Up @@ -226,6 +226,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
}

if plainSize >= config.DefaultMultipartMinSize {
return UploadFileStreamMultipart(ctx, cfg, targetFolderUUID, fileName, in, plainSize, modTime)
}
Expand Down
231 changes: 231 additions & 0 deletions buckets/upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -655,3 +655,234 @@ func TestUploadFileNameParsing(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)
}
})
}
}