package app import ( "context" "fmt" "io" "os" "path/filepath" "time" ) type mediaUploadObject struct { Key, ContentType string Size int64 Body io.Reader } type localMediaStorage struct{ directory string } func (s localMediaStorage) Bucket() string { return "" } func (s localMediaStorage) Put(_ context.Context, key, _ string, _ int64, body io.Reader) error { if err := os.MkdirAll(s.directory, 0o755); err != nil { return err } target := filepath.Join(s.directory, filepath.Base(key)) file, err := os.OpenFile(target, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o644) if err != nil { return err } // Never remove an existing file on collision. _, copyErr := io.Copy(file, body) closeErr := file.Close() if copyErr != nil || closeErr != nil { _ = os.Remove(target) if copyErr != nil { return copyErr } return closeErr } return nil } func (s localMediaStorage) Delete(_ context.Context, key string) error { return os.Remove(filepath.Join(s.directory, filepath.Base(key))) } // Return a cleanup function for both a partially failed upload and a failed DB // finalization. Cleanup gets its own deadline even if the HTTP client cancels. func putMediaObjects(ctx context.Context, storage mediaObjectStorage, objects []mediaUploadObject) (func(), error) { var uploaded []string cleanup := func() { cleanupCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 15*time.Second) defer cancel() for i := len(uploaded) - 1; i >= 0; i-- { _ = storage.Delete(cleanupCtx, uploaded[i]) } } for _, object := range objects { if err := storage.Put(ctx, object.Key, object.ContentType, object.Size, object.Body); err != nil { cleanup() return func() {}, fmt.Errorf("store media object: %w", err) } uploaded = append(uploaded, object.Key) } return cleanup, nil }