mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
268 lines
6.0 KiB
Go
268 lines
6.0 KiB
Go
package pkg
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
|
|
"github.com/stashapp/stash/pkg/models"
|
|
)
|
|
|
|
// SourcePathGetter gets the source path for a given package URL.
|
|
type SourcePathGetter interface {
|
|
// GetAllSourcePaths gets all source paths.
|
|
GetAllSourcePaths() []string
|
|
|
|
// GetSourcePath gets the source path for the given package URL.
|
|
GetSourcePath(srcURL string) string
|
|
}
|
|
|
|
// Manager manages the installation of paks.
|
|
type Manager struct {
|
|
Local *Store
|
|
PackagePathGetter SourcePathGetter
|
|
|
|
Client *http.Client
|
|
|
|
cache *repositoryCache
|
|
}
|
|
|
|
func (m *Manager) getCache() *repositoryCache {
|
|
if m.cache == nil {
|
|
m.cache = &repositoryCache{}
|
|
}
|
|
|
|
return m.cache
|
|
}
|
|
|
|
func (m *Manager) remoteFromURL(path string) (*httpRepository, error) {
|
|
u, err := url.Parse(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing path: %w", err)
|
|
}
|
|
|
|
return newHttpRepository(*u, m.Client, m.getCache()), nil
|
|
}
|
|
|
|
func (m *Manager) ListInstalled(ctx context.Context) (LocalPackageIndex, error) {
|
|
paths := m.PackagePathGetter.GetAllSourcePaths()
|
|
|
|
var installedList []Manifest
|
|
|
|
for _, p := range paths {
|
|
store := m.Local.sub(p)
|
|
|
|
srcList, err := store.List(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing local packages: %w", err)
|
|
}
|
|
|
|
installedList = append(installedList, srcList...)
|
|
}
|
|
|
|
return localPackageIndexFromList(installedList), nil
|
|
}
|
|
|
|
func (m *Manager) ListRemote(ctx context.Context, remoteURL string) (RemotePackageIndex, error) {
|
|
r, err := m.remoteFromURL(remoteURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("creating remote repository: %w", err)
|
|
}
|
|
|
|
list, err := r.List(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing remote packages: %w", err)
|
|
}
|
|
|
|
// add link to RemotePackage
|
|
for i := range list {
|
|
list[i].Repository = r
|
|
}
|
|
|
|
ret := remotePackageIndexFromList(list)
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (m *Manager) ListInstalledRemotes(ctx context.Context, installed LocalPackageIndex) (RemotePackageIndex, error) {
|
|
// get remotes for all installed packages
|
|
allRemoteList := make(RemotePackageIndex)
|
|
|
|
remoteURLs := installed.remoteURLs()
|
|
for _, remoteURL := range remoteURLs {
|
|
remoteList, err := m.ListRemote(ctx, remoteURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allRemoteList.merge(remoteList)
|
|
}
|
|
|
|
return allRemoteList, nil
|
|
}
|
|
|
|
func (m *Manager) InstalledStatus(ctx context.Context) (PackageStatusIndex, error) {
|
|
// get all installed packages
|
|
installed, err := m.ListInstalled(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// get remotes for all installed packages
|
|
allRemoteList, err := m.ListInstalledRemotes(ctx, installed)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ret := MakePackageStatusIndex(installed, allRemoteList)
|
|
|
|
return ret, nil
|
|
}
|
|
|
|
func (m *Manager) packageByID(ctx context.Context, spec models.PackageSpecInput) (*RemotePackage, error) {
|
|
l, err := m.ListRemote(ctx, spec.SourceURL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
pkg, found := l[spec]
|
|
if !found {
|
|
return nil, nil
|
|
}
|
|
|
|
return &pkg, nil
|
|
}
|
|
|
|
func (m *Manager) getStore(remoteURL string) *Store {
|
|
srcPath := m.PackagePathGetter.GetSourcePath(remoteURL)
|
|
store := m.Local.sub(srcPath)
|
|
|
|
return store
|
|
}
|
|
|
|
func (m *Manager) Install(ctx context.Context, spec models.PackageSpecInput) error {
|
|
remote, err := m.remoteFromURL(spec.SourceURL)
|
|
if err != nil {
|
|
return fmt.Errorf("creating remote repository: %w", err)
|
|
}
|
|
|
|
pkg, err := m.packageByID(ctx, spec)
|
|
if err != nil {
|
|
return fmt.Errorf("getting remote package: %w", err)
|
|
}
|
|
|
|
fromRemote, err := remote.GetPackageZip(ctx, *pkg)
|
|
if err != nil {
|
|
return fmt.Errorf("getting remote package: %w", err)
|
|
}
|
|
|
|
defer fromRemote.Close()
|
|
|
|
d, err := io.ReadAll(fromRemote)
|
|
if err != nil {
|
|
return fmt.Errorf("reading package data: %w", err)
|
|
}
|
|
|
|
sha := fmt.Sprintf("%x", sha256.Sum256(d))
|
|
if sha != pkg.Sha256 {
|
|
return fmt.Errorf("package data (%s) does not match expected SHA256 (%s)", sha, pkg.Sha256)
|
|
}
|
|
|
|
zr, err := zip.NewReader(bytes.NewReader(d), int64(len(d)))
|
|
if err != nil {
|
|
return fmt.Errorf("reading zip data: %w", err)
|
|
}
|
|
|
|
store := m.getStore(spec.SourceURL)
|
|
|
|
// uninstall existing package if present
|
|
if _, err := store.getManifest(ctx, pkg.ID); err == nil {
|
|
if err := m.deletePackageFiles(ctx, store, pkg.ID); err != nil {
|
|
return fmt.Errorf("uninstalling existing package: %w", err)
|
|
}
|
|
}
|
|
|
|
if err := m.installPackage(*pkg, store, zr); err != nil {
|
|
return fmt.Errorf("installing package: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) installPackage(pkg RemotePackage, store *Store, zr *zip.Reader) error {
|
|
manifest := Manifest{
|
|
ID: pkg.ID,
|
|
Name: pkg.Name,
|
|
Metadata: pkg.Metadata,
|
|
PackageVersion: pkg.PackageVersion,
|
|
RepositoryURL: pkg.Repository.Path(),
|
|
}
|
|
|
|
for _, f := range zr.File {
|
|
if f.FileInfo().IsDir() {
|
|
continue
|
|
}
|
|
|
|
i, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fn := filepath.Clean(f.Name)
|
|
if err := store.writeFile(pkg.ID, fn, f.Mode(), i); err != nil {
|
|
i.Close()
|
|
return fmt.Errorf("writing file %q: %w", fn, err)
|
|
}
|
|
|
|
i.Close()
|
|
manifest.Files = append(manifest.Files, fn)
|
|
}
|
|
|
|
if err := store.writeManifest(pkg.ID, manifest); err != nil {
|
|
return fmt.Errorf("writing manifest: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Uninstall uninstalls the given package.
|
|
func (m *Manager) Uninstall(ctx context.Context, spec models.PackageSpecInput) error {
|
|
store := m.getStore(spec.SourceURL)
|
|
|
|
if err := m.deletePackageFiles(ctx, store, spec.ID); err != nil {
|
|
return fmt.Errorf("deleting local package: %w", err)
|
|
}
|
|
|
|
// also delete the directory
|
|
// ignore errors
|
|
_ = store.deletePackageDir(spec.ID)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager) deletePackageFiles(ctx context.Context, store *Store, id string) error {
|
|
manifest, err := store.getManifest(ctx, id)
|
|
if err != nil {
|
|
return fmt.Errorf("getting manifest: %w", err)
|
|
}
|
|
|
|
for _, f := range manifest.Files {
|
|
if err := store.deleteFile(id, f); err != nil {
|
|
// ignore
|
|
continue
|
|
}
|
|
}
|
|
|
|
if err := store.deleteManifest(id); err != nil {
|
|
return fmt.Errorf("deleting manifest: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|