add local filesystem storage for packages and assets with upload handlers
This commit is contained in:
@@ -2,23 +2,27 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/omixlab/mosis-portal/internal/api/middleware"
|
||||
"github.com/omixlab/mosis-portal/internal/database"
|
||||
"github.com/omixlab/mosis-portal/internal/storage"
|
||||
)
|
||||
|
||||
// AppHandler handles app-related endpoints
|
||||
type AppHandler struct {
|
||||
db *database.DB
|
||||
db *database.DB
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// NewAppHandler creates a new app handler
|
||||
func NewAppHandler(db *database.DB) *AppHandler {
|
||||
return &AppHandler{db: db}
|
||||
func NewAppHandler(db *database.DB, store *storage.Storage) *AppHandler {
|
||||
return &AppHandler{db: db, storage: store}
|
||||
}
|
||||
|
||||
// CreateAppRequest is the request body for creating an app
|
||||
@@ -509,3 +513,144 @@ func isValidPackageID(id string) bool {
|
||||
}
|
||||
return dots >= 2
|
||||
}
|
||||
|
||||
// UploadPackage handles package file upload via multipart form
|
||||
func (h *AppHandler) UploadPackage(w http.ResponseWriter, r *http.Request) {
|
||||
developerID := middleware.GetDeveloperID(r.Context())
|
||||
if developerID == "" {
|
||||
Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
appID := chi.URLParam(r, "appID")
|
||||
versionID := chi.URLParam(r, "versionID")
|
||||
|
||||
// Verify app ownership
|
||||
app, err := h.db.GetApp(r.Context(), appID)
|
||||
if err != nil {
|
||||
Error(w, "app not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if app.DeveloperID != developerID {
|
||||
Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Get version
|
||||
version, err := h.db.GetVersion(r.Context(), versionID)
|
||||
if err != nil || version.AppID != appID {
|
||||
Error(w, "version not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Check version status - must be draft
|
||||
if version.Status != "draft" {
|
||||
Error(w, "package can only be uploaded to draft versions", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse multipart form (max 50MB)
|
||||
if err := r.ParseMultipartForm(50 << 20); err != nil {
|
||||
Error(w, "failed to parse form: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, header, err := r.FormFile("package")
|
||||
if err != nil {
|
||||
Error(w, "no package file provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Validate file extension
|
||||
if header.Filename[len(header.Filename)-6:] != ".mosis" {
|
||||
Error(w, "package file must have .mosis extension", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate upload ID and save to temp
|
||||
uploadID := uuid.New().String()
|
||||
size, err := h.storage.SaveTempFile(uploadID, file)
|
||||
if err != nil {
|
||||
Error(w, "failed to save package: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Validate package (signature, manifest extraction)
|
||||
// For now, just move to final location
|
||||
|
||||
// Move to final storage
|
||||
if err := h.storage.MoveTempToFinal(uploadID, developerID, appID, version.VersionCode); err != nil {
|
||||
h.storage.DeleteTemp(uploadID)
|
||||
Error(w, "failed to store package: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Update version record with package info
|
||||
packagePath := h.storage.PackagePath(developerID, appID, version.VersionCode)
|
||||
version.PackageURL = packagePath
|
||||
version.PackageSize = size
|
||||
|
||||
if err := h.db.UpdateVersionPackage(r.Context(), versionID, packagePath, size); err != nil {
|
||||
Error(w, "failed to update version: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"package_size": size,
|
||||
"package_url": fmt.Sprintf("/downloads/%s/%s/%d/package.mosis", developerID, appID, version.VersionCode),
|
||||
})
|
||||
}
|
||||
|
||||
// UploadIcon handles app icon upload
|
||||
func (h *AppHandler) UploadIcon(w http.ResponseWriter, r *http.Request) {
|
||||
developerID := middleware.GetDeveloperID(r.Context())
|
||||
if developerID == "" {
|
||||
Error(w, "unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
appID := chi.URLParam(r, "appID")
|
||||
|
||||
// Verify app ownership
|
||||
app, err := h.db.GetApp(r.Context(), appID)
|
||||
if err != nil {
|
||||
Error(w, "app not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
if app.DeveloperID != developerID {
|
||||
Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse multipart form (max 10MB)
|
||||
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
||||
Error(w, "failed to parse form: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, _, err := r.FormFile("icon")
|
||||
if err != nil {
|
||||
Error(w, "no icon file provided", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Process icon (validate, resize to multiple sizes)
|
||||
if err := h.storage.ProcessIcon(appID, file); err != nil {
|
||||
Error(w, "failed to process icon: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Return icon URLs
|
||||
iconURLs := make(map[string]string)
|
||||
for _, size := range storage.IconSizes {
|
||||
iconURLs[fmt.Sprintf("%d", size)] = fmt.Sprintf("/assets/%s/icon-%d.png", appID, size)
|
||||
}
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"success": true,
|
||||
"icons": iconURLs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/omixlab/mosis-portal/internal/database"
|
||||
"github.com/omixlab/mosis-portal/internal/storage"
|
||||
)
|
||||
|
||||
// StoreHandler handles public store endpoints
|
||||
type StoreHandler struct {
|
||||
db *database.DB
|
||||
db *database.DB
|
||||
storage *storage.Storage
|
||||
}
|
||||
|
||||
// NewStoreHandler creates a new store handler
|
||||
func NewStoreHandler(db *database.DB) *StoreHandler {
|
||||
return &StoreHandler{db: db}
|
||||
func NewStoreHandler(db *database.DB, store *storage.Storage) *StoreHandler {
|
||||
return &StoreHandler{db: db, storage: store}
|
||||
}
|
||||
|
||||
// ListApps returns published apps for the store
|
||||
@@ -69,14 +72,25 @@ func (h *StoreHandler) GetApp(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *StoreHandler) Download(w http.ResponseWriter, r *http.Request) {
|
||||
packageID := chi.URLParam(r, "packageID")
|
||||
|
||||
// Get app to find developer ID
|
||||
app, err := h.db.GetAppByPackageID(r.Context(), packageID)
|
||||
if err != nil {
|
||||
Error(w, "app not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
version, err := h.db.GetLatestPublishedVersion(r.Context(), packageID)
|
||||
if err != nil {
|
||||
Error(w, "no published version found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Build download URL
|
||||
downloadURL := fmt.Sprintf("/downloads/%s/%s/%d/package.mosis",
|
||||
app.DeveloperID, app.ID, version.VersionCode)
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"download_url": version.PackageURL,
|
||||
"download_url": downloadURL,
|
||||
"version": version.VersionName,
|
||||
"version_code": version.VersionCode,
|
||||
"size": version.PackageSize,
|
||||
@@ -95,14 +109,25 @@ func (h *StoreHandler) DownloadVersion(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Get app to find developer ID
|
||||
app, err := h.db.GetAppByPackageID(r.Context(), packageID)
|
||||
if err != nil {
|
||||
Error(w, "app not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
version, err := h.db.GetPublishedVersionByCode(r.Context(), packageID, versionCode)
|
||||
if err != nil {
|
||||
Error(w, "version not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Build download URL
|
||||
downloadURL := fmt.Sprintf("/downloads/%s/%s/%d/package.mosis",
|
||||
app.DeveloperID, app.ID, version.VersionCode)
|
||||
|
||||
JSON(w, http.StatusOK, map[string]interface{}{
|
||||
"download_url": version.PackageURL,
|
||||
"download_url": downloadURL,
|
||||
"version": version.VersionName,
|
||||
"version_code": version.VersionCode,
|
||||
"size": version.PackageSize,
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/omixlab/mosis-portal/internal/auth"
|
||||
"github.com/omixlab/mosis-portal/internal/config"
|
||||
"github.com/omixlab/mosis-portal/internal/database"
|
||||
"github.com/omixlab/mosis-portal/internal/storage"
|
||||
"github.com/omixlab/mosis-portal/internal/web"
|
||||
)
|
||||
|
||||
@@ -25,6 +26,12 @@ func NewRouter(cfg *config.Config, db *database.DB) http.Handler {
|
||||
r.Use(chimw.RealIP)
|
||||
r.Use(chimw.RequestID)
|
||||
|
||||
// Initialize storage
|
||||
store, err := storage.New(cfg.StoragePath)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to initialize storage: %v", err)
|
||||
}
|
||||
|
||||
// Initialize auth components
|
||||
jwtManager := auth.NewJWTManager(cfg.JWTSecret)
|
||||
oauthManager := auth.NewOAuthManager(
|
||||
@@ -34,8 +41,8 @@ func NewRouter(cfg *config.Config, db *database.DB) http.Handler {
|
||||
)
|
||||
authMiddleware := middleware.NewAuthMiddleware(jwtManager, db)
|
||||
authHandler := handlers.NewAuthHandler(oauthManager, jwtManager, db)
|
||||
appHandler := handlers.NewAppHandler(db)
|
||||
storeHandler := handlers.NewStoreHandler(db)
|
||||
appHandler := handlers.NewAppHandler(db, store)
|
||||
storeHandler := handlers.NewStoreHandler(db, store)
|
||||
|
||||
// Health check
|
||||
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -73,6 +80,9 @@ func NewRouter(cfg *config.Config, db *database.DB) http.Handler {
|
||||
r.Patch("/{appID}", appHandler.Update)
|
||||
r.Delete("/{appID}", appHandler.Delete)
|
||||
|
||||
// Icon upload
|
||||
r.Post("/{appID}/icon", appHandler.UploadIcon)
|
||||
|
||||
// Versions
|
||||
r.Route("/{appID}/versions", func(r chi.Router) {
|
||||
r.Get("/", appHandler.ListVersions)
|
||||
@@ -80,6 +90,9 @@ func NewRouter(cfg *config.Config, db *database.DB) http.Handler {
|
||||
r.Get("/{versionID}", appHandler.GetVersion)
|
||||
r.Post("/{versionID}/submit", appHandler.SubmitVersion)
|
||||
r.Post("/{versionID}/publish", appHandler.PublishVersion)
|
||||
|
||||
// Package upload
|
||||
r.Post("/{versionID}/upload", appHandler.UploadPackage)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -170,5 +183,14 @@ func NewRouter(cfg *config.Config, db *database.DB) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// Static file servers for packages and assets
|
||||
// Downloads - serve package files with proper headers
|
||||
r.Handle("/downloads/*", http.StripPrefix("/downloads/",
|
||||
http.FileServer(http.Dir(store.PackagesPath()))))
|
||||
|
||||
// Assets - serve icons and screenshots
|
||||
r.Handle("/assets/*", http.StripPrefix("/assets/",
|
||||
http.FileServer(http.Dir(store.AssetsPath()))))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ type Config struct {
|
||||
GoogleClientSecret string
|
||||
|
||||
// Storage
|
||||
PackagesDir string
|
||||
StoragePath string // Base path for all storage (packages/, assets/, temp/)
|
||||
BackupsDir string
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func Load() (*Config, error) {
|
||||
GoogleClientID: os.Getenv("GOOGLE_CLIENT_ID"),
|
||||
GoogleClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
|
||||
|
||||
PackagesDir: getEnv("PACKAGES_DIR", "./packages"),
|
||||
StoragePath: getEnv("STORAGE_PATH", "./storage"),
|
||||
BackupsDir: getEnv("BACKUPS_DIR", "./backups"),
|
||||
}
|
||||
|
||||
|
||||
@@ -698,6 +698,14 @@ func (db *DB) PublishVersion(ctx context.Context, id string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateVersionPackage updates the package_url and package_size for a version
|
||||
func (db *DB) UpdateVersionPackage(ctx context.Context, id, packageURL string, packageSize int64) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE app_versions SET package_url = ?, package_size = ? WHERE id = ?
|
||||
`, packageURL, packageSize, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// PublicApp represents a published app for the store
|
||||
type PublicApp struct {
|
||||
PackageID string `json:"package_id"`
|
||||
|
||||
64
portal/internal/storage/icons.go
Normal file
64
portal/internal/storage/icons.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io"
|
||||
|
||||
// Import image formats for decoding
|
||||
_ "image/gif"
|
||||
_ "image/jpeg"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
)
|
||||
|
||||
// IconSizes defines the icon sizes to generate
|
||||
var IconSizes = []int{32, 64, 128}
|
||||
|
||||
// ProcessIcon reads an image and generates icons at multiple sizes
|
||||
func (s *Storage) ProcessIcon(appID string, r io.Reader) error {
|
||||
// Decode the image
|
||||
img, _, err := image.Decode(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode image: %w", err)
|
||||
}
|
||||
|
||||
// Validate square dimensions
|
||||
bounds := img.Bounds()
|
||||
if bounds.Dx() != bounds.Dy() {
|
||||
return fmt.Errorf("icon must be square, got %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
|
||||
// Minimum size check
|
||||
if bounds.Dx() < 128 {
|
||||
return fmt.Errorf("icon must be at least 128x128, got %dx%d", bounds.Dx(), bounds.Dy())
|
||||
}
|
||||
|
||||
// Generate each size
|
||||
for _, size := range IconSizes {
|
||||
resized := resizeImage(img, size, size)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, resized); err != nil {
|
||||
return fmt.Errorf("failed to encode icon-%d: %w", size, err)
|
||||
}
|
||||
|
||||
if err := s.SaveIcon(appID, size, buf.Bytes()); err != nil {
|
||||
return fmt.Errorf("failed to save icon-%d: %w", size, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// resizeImage resizes an image to the specified dimensions using high-quality interpolation
|
||||
func resizeImage(src image.Image, width, height int) image.Image {
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// Use CatmullRom for high-quality downscaling
|
||||
draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
|
||||
|
||||
return dst
|
||||
}
|
||||
205
portal/internal/storage/storage.go
Normal file
205
portal/internal/storage/storage.go
Normal file
@@ -0,0 +1,205 @@
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user