Skip to content

Commit c193d0c

Browse files
committed
restore with latest
Signed-off-by: Avi Deitcher <avi@deitcher.net>
1 parent a97ed17 commit c193d0c

File tree

6 files changed

+152
-6
lines changed

6 files changed

+152
-6
lines changed

docs/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ for details of each.
157157
* `retention`: string, retention policy
158158
* `targets`: target configurations, each of which can be reference by other sections. Key is the name of the target that is referenced elsewhere. Each one has the following structure:
159159
* `type`: string, the type of target, one of: file, s3, smb
160-
* `url`: string, the URL of the target
160+
* `url`: string, the URL of the target; include `?latest` if the URL is a directory and you want to use the latest file in that directory. If the URL is a file, it should not include `?latest`.
161161
* `spec`: access details for the target, depends on target type:
162162
* Type s3:
163163
* `region`: string, the region

pkg/core/restore.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func (e *Executor) Restore(ctx context.Context, opts RestoreOptions) error {
4242
attribute.String("targetfile", opts.TargetFile),
4343
attribute.String("tmpfile", tmpRestoreFile),
4444
)
45-
copied, err := opts.Target.Pull(ctx, opts.TargetFile, tmpRestoreFile, logger)
45+
copied, err := opts.Target.Pull(ctx, opts.Target.URL(), tmpRestoreFile, logger)
4646
if err != nil {
4747
pullSpan.RecordError(err)
4848
pullSpan.End()

pkg/storage/file/file.go

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"io/fs"
88
"net/url"
99
"os"
10-
"path"
1110
"path/filepath"
1211

1312
log "github.com/sirupsen/logrus"
@@ -23,13 +22,60 @@ func New(u url.URL) *File {
2322
}
2423

2524
func (f *File) Pull(ctx context.Context, source, target string, logger *log.Entry) (int64, error) {
26-
return copyFile(path.Join(f.path, source), target)
25+
// see if the target has `?latest` set, if so, we need to find the latest file
26+
sourceFile := filepath.Join(f.path, source)
27+
u, err := url.Parse(sourceFile)
28+
if err != nil {
29+
return 0, fmt.Errorf("failed to parse target URL %s: %v", source, err)
30+
}
31+
q := u.Query()
32+
if q.Has("latest") {
33+
latestFilename, err := f.Latest(ctx, u.Path, logger)
34+
if err != nil {
35+
return 0, fmt.Errorf("failed to find latest file for source %s: %v", u.Path, err)
36+
}
37+
logger.Debugf("latest file for target %s is %s", u.Path, latestFilename)
38+
sourceFile = filepath.Join(u.Path, latestFilename)
39+
}
40+
41+
return copyFile(sourceFile, target)
2742
}
2843

2944
func (f *File) Push(ctx context.Context, target, source string, logger *log.Entry) (int64, error) {
3045
return copyFile(source, filepath.Join(f.path, target))
3146
}
3247

48+
func (f *File) Latest(ctx context.Context, target string, logger *log.Entry) (string, error) {
49+
fullTarget := filepath.Join(f.path, target)
50+
entries, err := os.ReadDir(fullTarget)
51+
if err != nil {
52+
return "", fmt.Errorf("failed to read directory %s: %w", f.path, err)
53+
}
54+
55+
var latest string
56+
var latestModTime int64
57+
58+
for _, entry := range entries {
59+
if entry.IsDir() || !entry.Type().IsRegular() {
60+
continue
61+
}
62+
info, err := entry.Info()
63+
if err != nil {
64+
return "", fmt.Errorf("failed to get info for file %s: %w", entry.Name(), err)
65+
}
66+
if info.ModTime().Unix() > latestModTime {
67+
latest = entry.Name()
68+
latestModTime = info.ModTime().Unix()
69+
}
70+
}
71+
72+
if latest == "" {
73+
return "", fmt.Errorf("no files found for target %s", target)
74+
}
75+
76+
return latest, nil
77+
}
78+
3379
func (f *File) Clean(filename string) string {
3480
return filename
3581
}

pkg/storage/s3/s3.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,14 +66,60 @@ func New(u url.URL, opts ...Option) *S3 {
6666
return s
6767
}
6868

69+
func (s *S3) Latest(ctx context.Context, target string, logger *log.Entry) (string, error) {
70+
// get the s3 client
71+
client, err := s.getClient(logger)
72+
if err != nil {
73+
return "", fmt.Errorf("failed to get AWS client: %v", err)
74+
}
75+
76+
// ensure that there is no leading /
77+
p := strings.TrimPrefix(filepath.Join(s.url.Path, target), "/")
78+
result, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{Bucket: aws.String(s.url.Hostname()), Prefix: aws.String(p)})
79+
if err != nil {
80+
return "", fmt.Errorf("failed to list objects, %v", err)
81+
}
82+
83+
var latest string
84+
var latestModTime time.Time
85+
86+
for _, item := range result.Contents {
87+
if item.LastModified.After(latestModTime) {
88+
latest = *item.Key
89+
latestModTime = *item.LastModified
90+
}
91+
}
92+
93+
if latest == "" {
94+
return "", fmt.Errorf("no files found for target %s", target)
95+
}
96+
97+
return latest, nil
98+
}
99+
69100
func (s *S3) Pull(ctx context.Context, source, target string, logger *log.Entry) (int64, error) {
70101
// get the s3 client
71102
client, err := s.getClient(logger)
72103
if err != nil {
73104
return 0, fmt.Errorf("failed to get AWS client: %v", err)
74105
}
75106

76-
bucket, path := s.url.Hostname(), path.Join(s.url.Path, source)
107+
sourceFile := filepath.Join(s.url.Path, source)
108+
u, err := url.Parse(sourceFile)
109+
if err != nil {
110+
return 0, fmt.Errorf("failed to parse target URL %s: %v", source, err)
111+
}
112+
q := u.Query()
113+
if q.Has("latest") {
114+
latestFilename, err := s.Latest(ctx, u.Path, logger)
115+
if err != nil {
116+
return 0, fmt.Errorf("failed to find latest file for source %s: %v", u.Path, err)
117+
}
118+
logger.Debugf("latest file for target %s is %s", u.Path, latestFilename)
119+
sourceFile = filepath.Join(u.Path, latestFilename)
120+
}
121+
122+
bucket, path := s.url.Hostname(), sourceFile
77123

78124
// Create a downloader with the session and default options
79125
downloader := manager.NewDownloader(client)

pkg/storage/smb/smb.go

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,12 +60,27 @@ func (s *SMB) Pull(ctx context.Context, source, target string, logger *log.Entry
6060
smbFilename := fmt.Sprintf("%s%c%s", sharepath, smb2.PathSeparator, filepath.Base(strings.ReplaceAll(target, ":", "-")))
6161
smbFilename = strings.TrimPrefix(smbFilename, fmt.Sprintf("%c", smb2.PathSeparator))
6262

63+
sourceFile := smbFilename
64+
u, err := url.Parse(smbFilename)
65+
if err != nil {
66+
return fmt.Errorf("failed to parse target URL %s: %v", source, err)
67+
}
68+
q := u.Query()
69+
if q.Has("latest") {
70+
latestFilename, err := s.Latest(ctx, u.Path, logger)
71+
if err != nil {
72+
return fmt.Errorf("failed to find latest file for target %s: %v", u.Path, err)
73+
}
74+
logger.Debugf("latest file for target %s is %s", u.Path, latestFilename)
75+
sourceFile = filepath.Join(u.Path, latestFilename)
76+
}
77+
6378
to, err := os.Create(target)
6479
if err != nil {
6580
return err
6681
}
6782
defer func() { _ = to.Close() }()
68-
from, err := fs.Open(smbFilename)
83+
from, err := fs.Open(sourceFile)
6984
if err != nil {
7085
return err
7186
}
@@ -76,6 +91,43 @@ func (s *SMB) Pull(ctx context.Context, source, target string, logger *log.Entry
7691
return copied, err
7792
}
7893

94+
func (s *SMB) Latest(ctx context.Context, target string, logger *log.Entry) (string, error) {
95+
var (
96+
latest string
97+
err error
98+
)
99+
err = s.exec(s.url, func(fs *smb2.Share, sharepath string) error {
100+
smbDirname := fmt.Sprintf("%s%c%s", sharepath, smb2.PathSeparator, target)
101+
smbDirname = strings.TrimPrefix(smbDirname, fmt.Sprintf("%c", smb2.PathSeparator))
102+
entries, err := fs.ReadDir(smbDirname)
103+
if err != nil {
104+
return fmt.Errorf("failed to read directory %s: %w", smbDirname, err)
105+
}
106+
107+
var latestModTime int64
108+
109+
for _, entry := range entries {
110+
if entry.IsDir() || !entry.Mode().IsRegular() {
111+
continue
112+
}
113+
114+
if entry.ModTime().Unix() > latestModTime {
115+
latest = entry.Name()
116+
latestModTime = entry.ModTime().Unix()
117+
}
118+
}
119+
120+
if latest == "" {
121+
return fmt.Errorf("no files found for target %s", target)
122+
}
123+
return nil
124+
})
125+
if err != nil {
126+
return "", err
127+
}
128+
return latest, nil
129+
}
130+
79131
func (s *SMB) Push(ctx context.Context, target, source string, logger *log.Entry) (int64, error) {
80132
var (
81133
copied int64

pkg/storage/storage.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ type Storage interface {
1313
Clean(filename string) string
1414
Push(ctx context.Context, target, source string, logger *log.Entry) (int64, error)
1515
Pull(ctx context.Context, source, target string, logger *log.Entry) (int64, error)
16+
// Latest returns the latest, or most recent, file for a given target. Should return just the filename, relative to `target`, not the path.
17+
Latest(ctx context.Context, target string, logger *log.Entry) (string, error)
1618
ReadDir(ctx context.Context, dirname string, logger *log.Entry) ([]fs.FileInfo, error)
1719
// Remove remove a particular file
1820
Remove(ctx context.Context, target string, logger *log.Entry) error

0 commit comments

Comments
 (0)