Commit 1f52b8ab by Denys Tito

fix: conservar nombre y tipo MIME reales en descargas cacheadas

DownloadFile devolvia el ID del archivo como nombre y deducia el tipo MIME
de la extension cuando la descarga salia de la cache, porque el archivo en
disco se guarda como <fileID><ext> y el nombre real nunca se conservaba.
La misma URL respondia distinto segun si la cache estaba fria o caliente.

Ahora el Service guarda path, nombre y tipo MIME en un indice en memoria al
cachear, y el TTL limpia indice y archivo juntos, de modo que un archivo
sobrante de una ejecucion anterior ya no se sirve indefinidamente.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
parent c7eccee7
...@@ -5,11 +5,11 @@ import ( ...@@ -5,11 +5,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"mime"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"sync"
"time" "time"
"golang.org/x/oauth2" "golang.org/x/oauth2"
...@@ -24,8 +24,16 @@ const oauthRedirectPath = "/oauth/callback" ...@@ -24,8 +24,16 @@ const oauthRedirectPath = "/oauth/callback"
const downloadCacheDir = "/var/tmp" const downloadCacheDir = "/var/tmp"
const downloadCacheTTL = 10 * time.Minute const downloadCacheTTL = 10 * time.Minute
type cachedDownload struct {
path string
name string
mimeType string
}
type Service struct { type Service struct {
svc *drive.Service svc *drive.Service
mu sync.RWMutex
cached map[string]cachedDownload
} }
func NewService(ctx context.Context, credentialsFile, tokenFile string) (*Service, error) { func NewService(ctx context.Context, credentialsFile, tokenFile string) (*Service, error) {
...@@ -57,7 +65,7 @@ func NewService(ctx context.Context, credentialsFile, tokenFile string) (*Servic ...@@ -57,7 +65,7 @@ func NewService(ctx context.Context, credentialsFile, tokenFile string) (*Servic
return nil, fmt.Errorf("creating drive service: %w", err) return nil, fmt.Errorf("creating drive service: %w", err)
} }
return &Service{svc: svc}, nil return &Service{svc: svc, cached: make(map[string]cachedDownload)}, nil
} }
func tokenFromFile(path string) (*oauth2.Token, error) { func tokenFromFile(path string) (*oauth2.Token, error) {
...@@ -149,16 +157,11 @@ func (s *Service) UploadFile(ctx context.Context, folderID string, fileHeader *m ...@@ -149,16 +157,11 @@ func (s *Service) UploadFile(ctx context.Context, folderID string, fileHeader *m
} }
func (s *Service) DownloadFile(ctx context.Context, fileID string) (io.ReadCloser, string, string, error) { func (s *Service) DownloadFile(ctx context.Context, fileID string) (io.ReadCloser, string, string, error) {
if path, ok := findCachedDownload(fileID); ok { if entry, ok := s.lookupCachedDownload(fileID); ok {
f, err := os.Open(path) if f, err := os.Open(entry.path); err == nil {
if err == nil { return f, entry.name, entry.mimeType, nil
name := filepath.Base(path)
mimeType := mime.TypeByExtension(filepath.Ext(path))
if mimeType == "" {
mimeType = "application/octet-stream"
}
return f, name, mimeType, nil
} }
s.forgetCachedDownload(fileID)
} }
meta, err := s.svc.Files.Get(fileID).SupportsAllDrives(true).Context(ctx).Fields("name, mimeType").Do() meta, err := s.svc.Files.Get(fileID).SupportsAllDrives(true).Context(ctx).Fields("name, mimeType").Do()
...@@ -177,18 +180,42 @@ func (s *Service) DownloadFile(ctx context.Context, fileID string) (io.ReadClose ...@@ -177,18 +180,42 @@ func (s *Service) DownloadFile(ctx context.Context, fileID string) (io.ReadClose
return nil, "", "", fmt.Errorf("caching downloaded file: %w", err) return nil, "", "", fmt.Errorf("caching downloaded file: %w", err)
} }
s.rememberCachedDownload(fileID, cachedDownload{
path: cachePath,
name: meta.Name,
mimeType: meta.MimeType,
})
return body, meta.Name, meta.MimeType, nil return body, meta.Name, meta.MimeType, nil
} }
func findCachedDownload(fileID string) (string, bool) { func (s *Service) lookupCachedDownload(fileID string) (cachedDownload, bool) {
if matches, err := filepath.Glob(filepath.Join(downloadCacheDir, fileID+".*")); err == nil && len(matches) > 0 { s.mu.RLock()
return matches[0], true defer s.mu.RUnlock()
}
path := filepath.Join(downloadCacheDir, fileID) entry, ok := s.cached[fileID]
if _, err := os.Stat(path); err == nil { return entry, ok
return path, true }
func (s *Service) rememberCachedDownload(fileID string, entry cachedDownload) {
s.mu.Lock()
s.cached[fileID] = entry
s.mu.Unlock()
time.AfterFunc(downloadCacheTTL, func() {
s.forgetCachedDownload(fileID)
})
}
func (s *Service) forgetCachedDownload(fileID string) {
s.mu.Lock()
entry, ok := s.cached[fileID]
delete(s.cached, fileID)
s.mu.Unlock()
if ok {
os.Remove(entry.path)
} }
return "", false
} }
func cacheDownload(body io.ReadCloser, path string) (io.ReadCloser, error) { func cacheDownload(body io.ReadCloser, path string) (io.ReadCloser, error) {
...@@ -205,9 +232,5 @@ func cacheDownload(body io.ReadCloser, path string) (io.ReadCloser, error) { ...@@ -205,9 +232,5 @@ func cacheDownload(body io.ReadCloser, path string) (io.ReadCloser, error) {
} }
f.Close() f.Close()
time.AfterFunc(downloadCacheTTL, func() {
os.Remove(path)
})
return os.Open(path) return os.Open(path)
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment