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
134 changes: 134 additions & 0 deletions resources/v1/files/client.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package files

import (
"fmt"
http "net/http"
"os"
"path/filepath"
"strings"

sdkcore "github.com/magichourhq/magic-hour-go/core"
upload_urls "github.com/magichourhq/magic-hour-go/resources/v1/files/upload_urls"
"github.com/magichourhq/magic-hour-go/types"
)

type Client struct {
Expand All @@ -22,3 +27,132 @@ func NewClient(coreClient *sdkcore.CoreClient) *Client {

return &client
}

// UploadFile uploads a local file to Magic Hour storage and returns the file path for use in API calls
//
// This function takes a local file path, determines the file type based on the extension,
// requests an upload URL, uploads the file, and returns the file path that can be used
// in API calls for image_file_path, video_file_path, or audio_file_path fields.
//
// Example:
//
// filePath, err := client.V1.Files.UploadFile("/path/to/your/image.jpg")
// if err != nil {
// // handle error
// }
// // Use filePath in API calls
//
// Supported file extensions:
// - Video: mp4, m4v, mov, webm
// - Audio: mp3, mpeg, wav, aac, aiff, flac
// - Image: png, jpg, jpeg, webp, avif, jp2, tiff, bmp
func (c *Client) UploadFile(localFilePath string, reqModifiers ...RequestModifier) (string, error) {
// Check if file exists
if _, err := os.Stat(localFilePath); os.IsNotExist(err) {
return "", fmt.Errorf("file does not exist: %s", localFilePath)
}

// Get file extension
ext := strings.TrimPrefix(filepath.Ext(localFilePath), ".")
if ext == "" {
return "", fmt.Errorf("file must have an extension: %s", localFilePath)
}

// Determine file type based on extension
fileType, err := c.determineFileType(ext)
if err != nil {
return "", err
}

// Create upload URL request
request := upload_urls.CreateRequest{
Items: []types.V1FilesUploadUrlsCreateBodyItemsItem{
{
Extension: ext,
Type: fileType,
},
},
}

// Get upload URL
response, err := c.UploadUrls.Create(request, reqModifiers...)
if err != nil {
return "", fmt.Errorf("failed to get upload URL: %w", err)
}

if len(response.Items) == 0 {
return "", fmt.Errorf("no upload URL received")
}

uploadItem := response.Items[0]

// Upload the file
err = c.uploadFileToURL(localFilePath, uploadItem.UploadUrl)
if err != nil {
return "", fmt.Errorf("failed to upload file: %w", err)
}

// Return the file path for use in API calls
return uploadItem.FilePath, nil
}

// determineFileType determines the file type based on the file extension
func (c *Client) determineFileType(extension string) (types.V1FilesUploadUrlsCreateBodyItemsItemTypeEnum, error) {
ext := strings.ToLower(extension)

// Video extensions
videoExts := []string{"mp4", "m4v", "mov", "webm"}
for _, videoExt := range videoExts {
if ext == videoExt {
return types.V1FilesUploadUrlsCreateBodyItemsItemTypeEnumVideo, nil
}
}

// Audio extensions
audioExts := []string{"mp3", "mpeg", "wav", "aac", "aiff", "flac"}
for _, audioExt := range audioExts {
if ext == audioExt {
return types.V1FilesUploadUrlsCreateBodyItemsItemTypeEnumAudio, nil
}
}

// Image extensions
imageExts := []string{"png", "jpg", "jpeg", "webp", "avif", "jp2", "tiff", "bmp"}
for _, imageExt := range imageExts {
if ext == imageExt {
return types.V1FilesUploadUrlsCreateBodyItemsItemTypeEnumImage, nil
}
}

return "", fmt.Errorf("unsupported file extension: %s", extension)
}

// uploadFileToURL uploads a file to the given pre-signed URL
func (c *Client) uploadFileToURL(localFilePath, uploadURL string) error {
// Open the file
file, err := os.Open(localFilePath)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()

// Create PUT request
req, err := http.NewRequest("PUT", uploadURL, file)
if err != nil {
return fmt.Errorf("failed to create upload request: %w", err)
}

// Execute the request
resp, err := c.coreClient.HttpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to upload file: %w", err)
}
defer resp.Body.Close()

// Check response status
if resp.StatusCode >= 300 {
return fmt.Errorf("upload failed with status %d", resp.StatusCode)
}

return nil
}
135 changes: 135 additions & 0 deletions tests/v1_files/resource_client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package test_files_client

import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"

sdk "github.com/magichourhq/magic-hour-go/client"
)

func TestUploadFileFunctionExists(t *testing.T) {
// Test that the UploadFile function exists and has the correct signature
client := sdk.NewClient(
sdk.WithBearerAuth("API_TOKEN"),
sdk.WithEnv(sdk.MockServer),
)

// Test that the function exists and can be called (will fail at runtime with mock server)
// but should not fail due to missing function or wrong signature
defer func() {
if r := recover(); r != nil {
t.Fatalf("UploadFile function not found or has wrong signature: %v", r)
}
}()

// This will fail at runtime because we're using a mock server, but it should not panic
// due to function signature issues
_, _ = client.V1.Files.UploadFile("/nonexistent/file.jpg")
}

func TestSupportedFileExtensions(t *testing.T) {
client := sdk.NewClient(sdk.WithEnv(sdk.MockServer))

// Test that supported extensions don't immediately fail with "unsupported extension" error
supportedExtensions := []string{
"mp4", "m4v", "mov", "webm", // Video
"mp3", "mpeg", "wav", "aac", "aiff", "flac", // Audio
"png", "jpg", "jpeg", "webp", "avif", "jp2", "tiff", "bmp", // Image
}

for _, extension := range supportedExtensions {
t.Run(fmt.Sprintf("supported_extension_%s", extension), func(t *testing.T) {
tempFile := createTempFile(t, extension)
defer os.Remove(tempFile)

_, err := client.V1.Files.UploadFile(tempFile)

// We expect an error (due to mock server), but NOT an "unsupported extension" error
if err != nil {
errorMsg := err.Error()
if strings.Contains(errorMsg, "unsupported file extension") {
t.Errorf("Extension %s should be supported, but got error: %s", extension, errorMsg)
}
}
})
}
}

func TestUnsupportedFileExtension(t *testing.T) {
client := sdk.NewClient(sdk.WithEnv(sdk.MockServer))

tempFile := createTempFile(t, "xyz")
defer os.Remove(tempFile)

_, err := client.V1.Files.UploadFile(tempFile)

if err == nil {
t.Error("Expected error for unsupported file extension, but got none")
}

if !strings.Contains(err.Error(), "unsupported file extension: xyz") {
t.Errorf("Expected 'unsupported file extension' error, got: %s", err.Error())
}
}

func TestUploadFileWithNonexistentFile(t *testing.T) {
client := sdk.NewClient(sdk.WithEnv(sdk.MockServer))

_, err := client.V1.Files.UploadFile("/nonexistent/file.jpg")

if err == nil {
t.Error("Expected error for nonexistent file, but got none")
}

expectedError := "file does not exist: /nonexistent/file.jpg"
if err.Error() != expectedError {
t.Errorf("Expected error message '%s', got '%s'", expectedError, err.Error())
}
}

func TestUploadFileWithNoExtension(t *testing.T) {
client := sdk.NewClient(sdk.WithEnv(sdk.MockServer))

// Create a temp file without extension
tempFile := createTempFile(t, "")
defer os.Remove(tempFile)

_, err := client.V1.Files.UploadFile(tempFile)

if err == nil {
t.Error("Expected error for file without extension, but got none")
}

expectedError := "file must have an extension: " + tempFile
if err.Error() != expectedError {
t.Errorf("Expected error message '%s', got '%s'", expectedError, err.Error())
}
}

// Helper function to create a temporary file with the given extension
func createTempFile(t *testing.T, extension string) string {
tempDir := t.TempDir()
filename := "test_file"
if extension != "" {
filename = filename + "." + extension
}

tempFile := filepath.Join(tempDir, filename)

file, err := os.Create(tempFile)
if err != nil {
t.Fatalf("Failed to create temp file: %v", err)
}
defer file.Close()

// Write some dummy content
_, err = file.WriteString("test content")
if err != nil {
t.Fatalf("Failed to write to temp file: %v", err)
}

return tempFile
}
Loading