// Package storage handles file storage for packages and assets package storage import ( "fmt" "io" "os" "path/filepath" ) // Storage manages file storage on the local filesystem type Storage struct { basePath string } // New creates a new Storage instance func New(basePath string) (*Storage, error) { // Create base directories if they don't exist dirs := []string{ filepath.Join(basePath, "packages"), filepath.Join(basePath, "assets"), filepath.Join(basePath, "temp"), } for _, dir := range dirs { if err := os.MkdirAll(dir, 0755); err != nil { return nil, fmt.Errorf("failed to create directory %s: %w", dir, err) } } return &Storage{basePath: basePath}, nil } // BasePath returns the base storage path func (s *Storage) BasePath() string { return s.basePath } // PackagesPath returns the path to the packages directory func (s *Storage) PackagesPath() string { return filepath.Join(s.basePath, "packages") } // AssetsPath returns the path to the assets directory func (s *Storage) AssetsPath() string { return filepath.Join(s.basePath, "assets") } // TempPath returns the path to the temp directory func (s *Storage) TempPath() string { return filepath.Join(s.basePath, "temp") } // PackagePath returns the path for a specific package version func (s *Storage) PackagePath(developerID, appID string, versionCode int) string { return filepath.Join(s.basePath, "packages", developerID, appID, fmt.Sprintf("%d", versionCode), "package.mosis") } // PackageDir returns the directory for a specific package version func (s *Storage) PackageDir(developerID, appID string, versionCode int) string { return filepath.Join(s.basePath, "packages", developerID, appID, fmt.Sprintf("%d", versionCode)) } // TempPackagePath returns a temp path for uploading func (s *Storage) TempPackagePath(uploadID string) string { return filepath.Join(s.basePath, "temp", uploadID, "package.mosis") } // IconPath returns the path for an app icon of a specific size func (s *Storage) IconPath(appID string, size int) string { return filepath.Join(s.basePath, "assets", appID, fmt.Sprintf("icon-%d.png", size)) } // ScreenshotPath returns the path for an app screenshot func (s *Storage) ScreenshotPath(appID string, index int) string { return filepath.Join(s.basePath, "assets", appID, "screenshots", fmt.Sprintf("%d.png", index)) } // SaveTempFile saves a file to temp storage func (s *Storage) SaveTempFile(uploadID string, r io.Reader) (int64, error) { path := s.TempPackagePath(uploadID) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return 0, fmt.Errorf("failed to create temp directory: %w", err) } f, err := os.Create(path) if err != nil { return 0, fmt.Errorf("failed to create temp file: %w", err) } defer f.Close() size, err := io.Copy(f, r) if err != nil { os.Remove(path) return 0, fmt.Errorf("failed to write temp file: %w", err) } return size, nil } // MoveTempToFinal moves a temp file to its final location func (s *Storage) MoveTempToFinal(uploadID, developerID, appID string, versionCode int) error { tempPath := s.TempPackagePath(uploadID) finalPath := s.PackagePath(developerID, appID, versionCode) // Create final directory if err := os.MkdirAll(filepath.Dir(finalPath), 0755); err != nil { return fmt.Errorf("failed to create package directory: %w", err) } // Move file if err := os.Rename(tempPath, finalPath); err != nil { // If rename fails (cross-device), copy and delete if err := copyFile(tempPath, finalPath); err != nil { return fmt.Errorf("failed to move temp file: %w", err) } os.Remove(tempPath) } // Clean up temp directory os.Remove(filepath.Dir(tempPath)) return nil } // DeleteTemp removes temp files for an upload func (s *Storage) DeleteTemp(uploadID string) error { tempDir := filepath.Join(s.basePath, "temp", uploadID) return os.RemoveAll(tempDir) } // SaveIcon saves an icon file func (s *Storage) SaveIcon(appID string, size int, data []byte) error { path := s.IconPath(appID, size) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return fmt.Errorf("failed to create assets directory: %w", err) } return os.WriteFile(path, data, 0644) } // SaveScreenshot saves a screenshot func (s *Storage) SaveScreenshot(appID string, index int, data []byte) error { path := s.ScreenshotPath(appID, index) if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return fmt.Errorf("failed to create screenshots directory: %w", err) } return os.WriteFile(path, data, 0644) } // OpenPackage opens a package file for reading func (s *Storage) OpenPackage(developerID, appID string, versionCode int) (*os.File, error) { path := s.PackagePath(developerID, appID, versionCode) return os.Open(path) } // PackageExists checks if a package file exists func (s *Storage) PackageExists(developerID, appID string, versionCode int) bool { path := s.PackagePath(developerID, appID, versionCode) _, err := os.Stat(path) return err == nil } // GetPackageSize returns the size of a package file func (s *Storage) GetPackageSize(developerID, appID string, versionCode int) (int64, error) { path := s.PackagePath(developerID, appID, versionCode) info, err := os.Stat(path) if err != nil { return 0, err } return info.Size(), nil } // DeletePackage removes a package and its directory func (s *Storage) DeletePackage(developerID, appID string, versionCode int) error { dir := s.PackageDir(developerID, appID, versionCode) return os.RemoveAll(dir) } // DeleteAppAssets removes all assets for an app func (s *Storage) DeleteAppAssets(appID string) error { dir := filepath.Join(s.basePath, "assets", appID) return os.RemoveAll(dir) } // GetPackagePath resolves a package URL (stored in DB) to a filesystem path // Package URLs are stored as relative paths like "packages/{developerID}/{appID}/{versionCode}/package.mosis" func (s *Storage) GetPackagePath(packageURL string) string { // If it's already an absolute path, return as-is if filepath.IsAbs(packageURL) { return packageURL } // Otherwise, join with base path return filepath.Join(s.basePath, packageURL) } // copyFile copies a file from src to dst func copyFile(src, dst string) error { in, err := os.Open(src) if err != nil { return err } defer in.Close() out, err := os.Create(dst) if err != nil { return err } defer out.Close() _, err = io.Copy(out, in) return err }