mirror of
https://github.com/aptly-dev/aptly.git
synced 2026-01-12 03:21:33 +00:00
instead of caching the whole s3 bucket, cache only the pool path. this requires an additional parameter, and since this is an interface, all implementations need to follow. might help in other backends too. closes #1181
314 lines
8.5 KiB
Go
314 lines
8.5 KiB
Go
package swift
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/aptly-dev/aptly/aptly"
|
|
"github.com/aptly-dev/aptly/utils"
|
|
"github.com/ncw/swift"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// PublishedStorage abstract file system with published files (actually hosted on Swift)
|
|
type PublishedStorage struct {
|
|
conn *swift.Connection
|
|
container string
|
|
prefix string
|
|
supportBulkDelete bool
|
|
}
|
|
|
|
type swiftInfo map[string]interface{}
|
|
|
|
// Check interface
|
|
var (
|
|
_ aptly.PublishedStorage = (*PublishedStorage)(nil)
|
|
)
|
|
|
|
// NewPublishedStorage creates new instance of PublishedStorage with specified Swift access
|
|
// keys, tenant and tenantId
|
|
func NewPublishedStorage(username string, password string, authURL string, tenant string, tenantID string, domain string, domainID string, tenantDomain string, tenantDomainID string, container string, prefix string) (*PublishedStorage, error) {
|
|
if username == "" {
|
|
if username = os.Getenv("OS_USERNAME"); username == "" {
|
|
username = os.Getenv("ST_USER")
|
|
}
|
|
}
|
|
if password == "" {
|
|
if password = os.Getenv("OS_PASSWORD"); password == "" {
|
|
password = os.Getenv("ST_KEY")
|
|
}
|
|
}
|
|
if authURL == "" {
|
|
if authURL = os.Getenv("OS_AUTH_URL"); authURL == "" {
|
|
authURL = os.Getenv("ST_AUTH")
|
|
}
|
|
}
|
|
if tenant == "" {
|
|
tenant = os.Getenv("OS_TENANT_NAME")
|
|
}
|
|
if tenantID == "" {
|
|
tenantID = os.Getenv("OS_TENANT_ID")
|
|
}
|
|
if domain == "" {
|
|
domain = os.Getenv("OS_USER_DOMAIN_NAME")
|
|
}
|
|
if domainID == "" {
|
|
domainID = os.Getenv("OS_USER_DOMAIN_ID")
|
|
}
|
|
if tenantDomain == "" {
|
|
tenantDomain = os.Getenv("OS_PROJECT_DOMAIN")
|
|
}
|
|
if tenantDomainID == "" {
|
|
tenantDomainID = os.Getenv("OS_PROJECT_DOMAIN_ID")
|
|
}
|
|
|
|
ct := &swift.Connection{
|
|
UserName: username,
|
|
ApiKey: password,
|
|
AuthUrl: authURL,
|
|
UserAgent: "aptly/" + aptly.Version,
|
|
Tenant: tenant,
|
|
TenantId: tenantID,
|
|
Domain: domain,
|
|
DomainId: domainID,
|
|
TenantDomain: tenantDomain,
|
|
TenantDomainId: tenantDomainID,
|
|
ConnectTimeout: 60 * time.Second,
|
|
Timeout: 60 * time.Second,
|
|
}
|
|
err := ct.Authenticate()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("swift authentication failed: %s", err)
|
|
}
|
|
|
|
var bulkDelete bool
|
|
resp, err := http.Get(filepath.Join(ct.StorageUrl, "..", "..") + "/info")
|
|
if err == nil {
|
|
defer resp.Body.Close()
|
|
decoder := json.NewDecoder(resp.Body)
|
|
var infos swiftInfo
|
|
if decoder.Decode(&infos) == nil {
|
|
_, bulkDelete = infos["bulk_delete"]
|
|
}
|
|
}
|
|
|
|
result := &PublishedStorage{
|
|
conn: ct,
|
|
container: container,
|
|
prefix: prefix,
|
|
supportBulkDelete: bulkDelete,
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// String
|
|
func (storage *PublishedStorage) String() string {
|
|
return fmt.Sprintf("Swift: %s/%s/%s", storage.conn.StorageUrl, storage.container, storage.prefix)
|
|
}
|
|
|
|
// MkDir creates directory recursively under public path
|
|
func (storage *PublishedStorage) MkDir(_ string) error {
|
|
// no op for Swift
|
|
return nil
|
|
}
|
|
|
|
// PutFile puts file into published storage at specified path
|
|
func (storage *PublishedStorage) PutFile(path string, sourceFilename string) error {
|
|
var (
|
|
source *os.File
|
|
err error
|
|
)
|
|
source, err = os.Open(sourceFilename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer source.Close()
|
|
|
|
err = storage.putFile(path, source)
|
|
if err != nil {
|
|
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s", sourceFilename, storage))
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (storage *PublishedStorage) putFile(path string, source io.Reader) error {
|
|
_, err := storage.conn.ObjectPut(storage.container, filepath.Join(storage.prefix, path), source, false, "", "", nil)
|
|
|
|
return err
|
|
}
|
|
|
|
// Remove removes single file under public path
|
|
func (storage *PublishedStorage) Remove(path string) error {
|
|
err := storage.conn.ObjectDelete(storage.container, filepath.Join(storage.prefix, path))
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("error deleting %s from %s: %s", path, storage, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RemoveDirs removes directory structure under public path
|
|
func (storage *PublishedStorage) RemoveDirs(path string, _ aptly.Progress) error {
|
|
path = filepath.Join(storage.prefix, path)
|
|
opts := swift.ObjectsOpts{
|
|
Prefix: path,
|
|
}
|
|
|
|
objects, err := storage.conn.ObjectNamesAll(storage.container, &opts)
|
|
if err != nil {
|
|
return fmt.Errorf("error removing dir %s from %s: %s", path, storage, err)
|
|
}
|
|
|
|
for index, name := range objects {
|
|
objects[index] = name[len(storage.prefix):]
|
|
}
|
|
|
|
multiDelete := true
|
|
if storage.supportBulkDelete {
|
|
_, err := storage.conn.BulkDelete(storage.container, objects)
|
|
multiDelete = err != nil
|
|
}
|
|
if multiDelete {
|
|
for _, name := range objects {
|
|
if err := storage.conn.ObjectDelete(storage.container, name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LinkFromPool links package file from pool to dist's pool location
|
|
//
|
|
// publishedDirectory is desired location in pool (like prefix/pool/component/liba/libav/)
|
|
// sourcePool is instance of aptly.PackagePool
|
|
// sourcePath is filepath to package file in package pool
|
|
//
|
|
// LinkFromPool returns relative path for the published file to be included in package index
|
|
func (storage *PublishedStorage) LinkFromPool(prefix string, path string, fileName string, sourcePool aptly.PackagePool,
|
|
sourcePath string, sourceChecksums utils.ChecksumInfo, force bool) error {
|
|
|
|
publishedDirectory := filepath.Join(prefix, path)
|
|
relPath := filepath.Join(publishedDirectory, fileName)
|
|
poolPath := filepath.Join(storage.prefix, relPath)
|
|
|
|
var (
|
|
info swift.Object
|
|
err error
|
|
)
|
|
|
|
info, _, err = storage.conn.Object(storage.container, poolPath)
|
|
if err != nil {
|
|
if err != swift.ObjectNotFound {
|
|
return fmt.Errorf("error getting information about %s from %s: %s", poolPath, storage, err)
|
|
}
|
|
} else {
|
|
if sourceChecksums.MD5 == "" {
|
|
return fmt.Errorf("unable to compare object, MD5 checksum missing")
|
|
}
|
|
|
|
if !force && info.Hash != sourceChecksums.MD5 {
|
|
return fmt.Errorf("error putting file to %s: file already exists and is different: %s", poolPath, storage)
|
|
|
|
}
|
|
}
|
|
|
|
source, err := sourcePool.Open(sourcePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer source.Close()
|
|
|
|
err = storage.putFile(relPath, source)
|
|
if err != nil {
|
|
err = errors.Wrap(err, fmt.Sprintf("error uploading %s to %s: %s", sourcePath, storage, poolPath))
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// Filelist returns list of files under prefix
|
|
func (storage *PublishedStorage) Filelist(prefix string) ([]string, error) {
|
|
prefix = filepath.Join(storage.prefix, prefix)
|
|
if prefix != "" {
|
|
prefix += "/"
|
|
}
|
|
opts := swift.ObjectsOpts{
|
|
Prefix: prefix,
|
|
}
|
|
contents, err := storage.conn.ObjectNamesAll(storage.container, &opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error listing under prefix %s in %s: %s", prefix, storage, err)
|
|
}
|
|
|
|
for index, name := range contents {
|
|
contents[index] = name[len(prefix):]
|
|
}
|
|
|
|
return contents, nil
|
|
}
|
|
|
|
// RenameFile renames (moves) file
|
|
func (storage *PublishedStorage) RenameFile(oldName, newName string) error {
|
|
err := storage.conn.ObjectMove(storage.container, filepath.Join(storage.prefix, oldName), storage.container, filepath.Join(storage.prefix, newName))
|
|
if err != nil {
|
|
return fmt.Errorf("error copying %s -> %s in %s: %s", oldName, newName, storage, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// SymLink creates a copy of src file and adds link information as meta data
|
|
func (storage *PublishedStorage) SymLink(src string, dst string) error {
|
|
srcObjectName := filepath.Join(storage.prefix, src)
|
|
dstObjectName := filepath.Join(storage.prefix, dst)
|
|
|
|
headers := map[string]string{
|
|
"SymLink": srcObjectName,
|
|
}
|
|
_, err := storage.conn.ObjectCopy(storage.container, srcObjectName, storage.container, dstObjectName, headers)
|
|
if err != nil {
|
|
return fmt.Errorf("error symlinking %s -> %s in %s: %s", srcObjectName, dstObjectName, storage, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// HardLink using symlink functionality as hard links do not exist
|
|
func (storage *PublishedStorage) HardLink(src string, dst string) error {
|
|
return storage.SymLink(src, dst)
|
|
}
|
|
|
|
// FileExists returns true if path exists
|
|
func (storage *PublishedStorage) FileExists(path string) (bool, error) {
|
|
_, _, err := storage.conn.Object(storage.container, filepath.Join(storage.prefix, path))
|
|
|
|
if err != nil {
|
|
if err == swift.ObjectNotFound {
|
|
return false, nil
|
|
}
|
|
|
|
return false, err
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// ReadLink returns the symbolic link pointed to by path
|
|
func (storage *PublishedStorage) ReadLink(path string) (string, error) {
|
|
srcObjectName := filepath.Join(storage.prefix, path)
|
|
_, headers, err := storage.conn.Object(storage.container, srcObjectName)
|
|
if err != nil {
|
|
return "", fmt.Errorf("error reading symlink %s in %s: %s", srcObjectName, storage, err)
|
|
}
|
|
|
|
return headers["SymLink"], nil
|
|
}
|