Initial updates server repository commit.
Reinitialize repository history and exclude generated OTA artifact outputs. Made-with: Cursor
This commit is contained in:
169
internal/storage/local.go
Normal file
169
internal/storage/local.go
Normal file
@@ -0,0 +1,169 @@
|
||||
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
|
||||
}
|
||||
|
||||
55
internal/storage/zip.go
Normal file
55
internal/storage/zip.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func unzipToDir(readerAt io.ReaderAt, size int64, target string) error {
|
||||
if err := os.MkdirAll(target, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
zr, err := zip.NewReader(readerAt, size)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, f := range zr.File {
|
||||
name := filepath.Clean(f.Name)
|
||||
if strings.HasPrefix(name, "..") {
|
||||
return fmt.Errorf("invalid zip entry path: %s", f.Name)
|
||||
}
|
||||
fullPath := filepath.Join(target, name)
|
||||
if f.FileInfo().IsDir() {
|
||||
if err := os.MkdirAll(fullPath, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(fullPath), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
src, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dst, err := os.Create(fullPath)
|
||||
if err != nil {
|
||||
src.Close()
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
dst.Close()
|
||||
src.Close()
|
||||
return err
|
||||
}
|
||||
dst.Close()
|
||||
src.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user