Reinitialize repository history and exclude generated OTA artifact outputs. Made-with: Cursor
170 lines
4.4 KiB
Go
170 lines
4.4 KiB
Go
package storage
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"expo-updates-server-go/internal/model"
|
|
)
|
|
|
|
var ErrUnsupportedRuntime = errors.New("Unsupported runtime version")
|
|
var ErrUnsupportedPlatform = errors.New("Unsupported platform")
|
|
|
|
type LocalStore struct {
|
|
root string
|
|
}
|
|
|
|
func NewLocalStore(root string) *LocalStore {
|
|
return &LocalStore{root: root}
|
|
}
|
|
|
|
func (s *LocalStore) UpdatesRoot() string {
|
|
return s.root
|
|
}
|
|
|
|
func (s *LocalStore) LatestReleasePath(runtimeVersion string, platform string) (string, error) {
|
|
if platform != "ios" && platform != "android" {
|
|
return "", ErrUnsupportedPlatform
|
|
}
|
|
runtimeDir := filepath.Join(s.root, runtimeVersion)
|
|
runtimeStat, err := os.Stat(runtimeDir)
|
|
if err != nil || !runtimeStat.IsDir() {
|
|
return "", ErrUnsupportedRuntime
|
|
}
|
|
platformDir := filepath.Join(runtimeDir, platform)
|
|
platformStat, err := os.Stat(platformDir)
|
|
if err != nil || !platformStat.IsDir() {
|
|
return "", ErrUnsupportedPlatform
|
|
}
|
|
return platformDir, nil
|
|
}
|
|
|
|
func (s *LocalStore) HasRollbackMarker(releasePath string) bool {
|
|
_, err := os.Stat(filepath.Join(releasePath, "rollback"))
|
|
return err == nil
|
|
}
|
|
|
|
func (s *LocalStore) ReadMetadata(releasePath string) (model.MetadataFile, []byte, time.Time, error) {
|
|
var out model.MetadataFile
|
|
metaPath := filepath.Join(releasePath, "metadata.json")
|
|
b, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
return out, nil, time.Time{}, err
|
|
}
|
|
if err := json.Unmarshal(b, &out); err != nil {
|
|
return out, nil, time.Time{}, err
|
|
}
|
|
fi, err := os.Stat(metaPath)
|
|
if err != nil {
|
|
return out, nil, time.Time{}, err
|
|
}
|
|
return out, b, fi.ModTime(), nil
|
|
}
|
|
|
|
func (s *LocalStore) ReadExpoConfig(releasePath string) (map[string]any, error) {
|
|
filePath := filepath.Join(releasePath, "expoConfig.json")
|
|
b, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out map[string]any
|
|
if err := json.Unmarshal(b, &out); err != nil {
|
|
return nil, err
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (s *LocalStore) OpenAssetSafe(asset string) ([]byte, error) {
|
|
clean := filepath.Clean(asset)
|
|
if strings.HasPrefix(clean, "..") {
|
|
return nil, fmt.Errorf("invalid asset path")
|
|
}
|
|
return os.ReadFile(clean)
|
|
}
|
|
|
|
func (s *LocalStore) PublishZip(runtimeVersion string, platform string, zipReader io.ReaderAt, size int64) (string, error) {
|
|
if platform != "ios" && platform != "android" {
|
|
return "", fmt.Errorf("%w: %s", ErrUnsupportedPlatform, platform)
|
|
}
|
|
runtimeDir := filepath.Join(s.root, runtimeVersion)
|
|
if err := os.MkdirAll(runtimeDir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
targetDir := filepath.Join(runtimeDir, platform)
|
|
tmpDir := targetDir + ".tmp"
|
|
_ = os.RemoveAll(tmpDir)
|
|
_ = os.RemoveAll(targetDir)
|
|
|
|
if err := unzipToDir(zipReader, size, tmpDir); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
manifestPath := filepath.Join(tmpDir, "metadata.json")
|
|
if _, err := os.Stat(manifestPath); err != nil {
|
|
return "", fmt.Errorf("published zip must contain metadata.json at root")
|
|
}
|
|
|
|
if err := os.Rename(tmpDir, targetDir); err != nil {
|
|
return "", err
|
|
}
|
|
return targetDir, nil
|
|
}
|
|
|
|
func (s *LocalStore) CreateRollback(runtimeVersion string, platform string) (string, error) {
|
|
if platform != "ios" && platform != "android" {
|
|
return "", fmt.Errorf("%w: %s", ErrUnsupportedPlatform, platform)
|
|
}
|
|
releaseDir := filepath.Join(s.root, runtimeVersion, platform)
|
|
if _, err := os.Stat(releaseDir); err != nil {
|
|
return "", err
|
|
}
|
|
path := filepath.Join(releaseDir, "rollback")
|
|
if err := os.WriteFile(path, []byte{}, 0o644); err != nil {
|
|
return "", err
|
|
}
|
|
return path, nil
|
|
}
|
|
|
|
func (s *LocalStore) ListReleases(runtimeVersion string) ([]model.ReleaseInfo, error) {
|
|
runtimeDir := filepath.Join(s.root, runtimeVersion)
|
|
runtimeStat, err := os.Stat(runtimeDir)
|
|
if err != nil || !runtimeStat.IsDir() {
|
|
return nil, ErrUnsupportedRuntime
|
|
}
|
|
entries, err := os.ReadDir(runtimeDir)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]model.ReleaseInfo, 0, len(entries))
|
|
for _, e := range entries {
|
|
if !e.IsDir() {
|
|
continue
|
|
}
|
|
if e.Name() != "ios" && e.Name() != "android" {
|
|
continue
|
|
}
|
|
full := filepath.Join(runtimeDir, e.Name())
|
|
fi, err := os.Stat(full)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
out = append(out, model.ReleaseInfo{
|
|
RuntimeVersion: runtimeVersion,
|
|
ReleaseID: e.Name(),
|
|
Path: full,
|
|
CreatedAt: fi.ModTime(),
|
|
})
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].ReleaseID < out[j].ReleaseID })
|
|
return out, nil
|
|
}
|
|
|