Update application UI and functionality
This commit is contained in:
@@ -61,6 +61,19 @@ type UploadOptions struct {
|
||||
UpdateManifest bool
|
||||
}
|
||||
|
||||
type UploadedPackageFile struct {
|
||||
TempPath string
|
||||
Size int64
|
||||
SHA256 string
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUploadedPackageMissing = errors.New("uploaded file is missing")
|
||||
ErrUploadedPackageEmpty = errors.New("uploaded file is empty")
|
||||
ErrUploadedPackageStorageFailed = errors.New("upload storage failed")
|
||||
ErrUploadedPackageManifestFailed = errors.New("manifest update failed")
|
||||
)
|
||||
|
||||
func NewService(cfg *config.Config, store *db.Store, noticeService ...*notices.Service) *Service {
|
||||
service := &Service{cfg: cfg, store: store, hashes: map[string]cachedFileHash{}}
|
||||
if len(noticeService) > 0 {
|
||||
@@ -226,7 +239,7 @@ func (s *Service) SaveUploadedPackage(r *http.Request, reader io.Reader, opts Up
|
||||
return Package{}, err
|
||||
}
|
||||
if err := os.MkdirAll(s.cfg.DownloadsDir, 0o750); err != nil {
|
||||
return Package{}, err
|
||||
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
|
||||
}
|
||||
target := filepath.Join(s.cfg.DownloadsDir, name)
|
||||
resolved, err := filepath.Abs(target)
|
||||
@@ -288,6 +301,108 @@ func (s *Service) SaveUploadedPackage(r *http.Request, reader io.Reader, opts Up
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func (s *Service) SavePreparedPackage(r *http.Request, uploaded UploadedPackageFile, opts UploadOptions, actor string) (Package, error) {
|
||||
name, err := safePackageName(opts.FileName)
|
||||
if err != nil {
|
||||
return Package{}, err
|
||||
}
|
||||
if uploaded.TempPath == "" {
|
||||
return Package{}, ErrUploadedPackageMissing
|
||||
}
|
||||
if uploaded.Size <= 0 {
|
||||
return Package{}, ErrUploadedPackageEmpty
|
||||
}
|
||||
if err := os.MkdirAll(s.cfg.DownloadsDir, 0o750); err != nil {
|
||||
return Package{}, err
|
||||
}
|
||||
target := filepath.Join(s.cfg.DownloadsDir, name)
|
||||
resolved, err := filepath.Abs(target)
|
||||
if err != nil {
|
||||
return Package{}, err
|
||||
}
|
||||
base, _ := filepath.Abs(s.cfg.DownloadsDir)
|
||||
if resolved != base && !strings.HasPrefix(resolved, base+string(os.PathSeparator)) {
|
||||
return Package{}, errors.New("path escape rejected")
|
||||
}
|
||||
if err := os.Chmod(uploaded.TempPath, 0o640); err != nil {
|
||||
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
|
||||
}
|
||||
backup, err := backupExistingPackage(target)
|
||||
if err != nil {
|
||||
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
|
||||
}
|
||||
if err := os.Rename(uploaded.TempPath, target); err != nil {
|
||||
_ = restorePackageBackup(target, backup)
|
||||
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageStorageFailed, err)
|
||||
}
|
||||
version := firstNonEmpty(opts.Version, detectVersion(name))
|
||||
platform, arch := detectPlatform(name)
|
||||
platform = firstNonEmpty(opts.Platform, platform)
|
||||
arch = firstNonEmpty(opts.Arch, arch)
|
||||
product := detectProduct(name)
|
||||
pkg := Package{
|
||||
ID: strings.ToLower(strings.ReplaceAll(product+"-"+platform+"-"+arch+"-"+version, " ", "-")),
|
||||
Name: product,
|
||||
Version: version,
|
||||
Platform: platform,
|
||||
Arch: arch,
|
||||
URL: firstNonEmpty(strings.TrimRight(s.cfg.CDNBaseURL, "/"), requestBaseURL(r, s.cfg.BaseURL)) + "/downloads/" + name,
|
||||
SHA256: strings.ToLower(uploaded.SHA256),
|
||||
Size: uploaded.Size,
|
||||
Required: strings.Contains(strings.ToLower(product), "ymhut"),
|
||||
Enabled: true,
|
||||
FileName: name,
|
||||
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
if opts.UpdateManifest {
|
||||
if err := s.updateLegacyManifest(pkg, opts); err != nil {
|
||||
if rollbackErr := restorePackageBackup(target, backup); rollbackErr != nil {
|
||||
return Package{}, fmt.Errorf("%w: %v; rollback failed: %v", ErrUploadedPackageManifestFailed, err, rollbackErr)
|
||||
}
|
||||
return Package{}, fmt.Errorf("%w: %v", ErrUploadedPackageManifestFailed, err)
|
||||
}
|
||||
}
|
||||
if backup != "" {
|
||||
_ = os.Remove(backup)
|
||||
}
|
||||
_ = s.store.InsertAudit(db.AuditLog{Actor: firstNonEmpty(actor, "admin"), Type: "release.package_uploaded", Target: name, Message: fmt.Sprintf("已上传发布包 %s(%s,%s)", name, version, formatBytes(uploaded.Size))})
|
||||
return pkg, nil
|
||||
}
|
||||
|
||||
func backupExistingPackage(target string) (string, error) {
|
||||
if _, err := os.Stat(target); errors.Is(err, os.ErrNotExist) {
|
||||
return "", nil
|
||||
} else if err != nil {
|
||||
return "", err
|
||||
}
|
||||
file, err := os.CreateTemp(filepath.Dir(target), "."+filepath.Base(target)+".backup-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
backup := file.Name()
|
||||
if err := file.Close(); err != nil {
|
||||
_ = os.Remove(backup)
|
||||
return "", err
|
||||
}
|
||||
if err := os.Remove(backup); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.Rename(target, backup); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return backup, nil
|
||||
}
|
||||
|
||||
func restorePackageBackup(target, backup string) error {
|
||||
if err := os.Remove(target); err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return err
|
||||
}
|
||||
if backup == "" {
|
||||
return nil
|
||||
}
|
||||
return os.Rename(backup, target)
|
||||
}
|
||||
|
||||
func (s *Service) updateLegacyManifest(pkg Package, opts UploadOptions) error {
|
||||
path := filepath.Join(s.cfg.UpdatePublicDir, "update-info.json")
|
||||
payload := s.legacyUpdateBase()
|
||||
|
||||
Reference in New Issue
Block a user