mirror of
https://github.com/Luzifer/s3sync.git
synced 2024-12-20 19:41:15 +00:00
Initial version
This commit is contained in:
commit
c90c2fb87e
8 changed files with 488 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
s3sync
|
||||
Godeps/_workspace/
|
5
.gobuilder.yml
Normal file
5
.gobuilder.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
build_matrix:
|
||||
general:
|
||||
ldflags:
|
||||
- "-X main.version $(git describe --tags)"
|
62
Godeps/Godeps.json
generated
Normal file
62
Godeps/Godeps.json
generated
Normal file
|
@ -0,0 +1,62 @@
|
|||
{
|
||||
"ImportPath": "github.com/Luzifer/s3sync",
|
||||
"GoVersion": "go1.4.2",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/aws",
|
||||
"Comment": "v0.6.4-2-g168a70b",
|
||||
"Rev": "168a70b9c21a4f60166d7925b690356605907adb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/internal/endpoints",
|
||||
"Comment": "v0.6.4-2-g168a70b",
|
||||
"Rev": "168a70b9c21a4f60166d7925b690356605907adb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/internal/protocol/query",
|
||||
"Comment": "v0.6.4-2-g168a70b",
|
||||
"Rev": "168a70b9c21a4f60166d7925b690356605907adb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/internal/protocol/rest",
|
||||
"Comment": "v0.6.4-2-g168a70b",
|
||||
"Rev": "168a70b9c21a4f60166d7925b690356605907adb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/internal/protocol/restxml",
|
||||
"Comment": "v0.6.4-2-g168a70b",
|
||||
"Rev": "168a70b9c21a4f60166d7925b690356605907adb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/internal/protocol/xml/xmlutil",
|
||||
"Comment": "v0.6.4-2-g168a70b",
|
||||
"Rev": "168a70b9c21a4f60166d7925b690356605907adb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/internal/signer/v4",
|
||||
"Comment": "v0.6.4-2-g168a70b",
|
||||
"Rev": "168a70b9c21a4f60166d7925b690356605907adb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/aws/aws-sdk-go/service/s3",
|
||||
"Comment": "v0.6.4-2-g168a70b",
|
||||
"Rev": "168a70b9c21a4f60166d7925b690356605907adb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/inconshreveable/mousetrap",
|
||||
"Rev": "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/spf13/cobra",
|
||||
"Rev": "385fc87e4343efec233811d3d933509e8975d11a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/spf13/pflag",
|
||||
"Rev": "67cbc198fd11dab704b214c1e629a97af392c085"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vaughan0/go-ini",
|
||||
"Rev": "a98ad7ee00ec53921f08832bc06ecf7fd600e6a1"
|
||||
}
|
||||
]
|
||||
}
|
13
LICENSE
Normal file
13
LICENSE
Normal file
|
@ -0,0 +1,13 @@
|
|||
Copyright 2015 Knut Ahlers <knut@ahlers.me>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
55
README.md
Normal file
55
README.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Luzifer / s3sync
|
||||
|
||||
[![License: Apache v2.0](https://badge.luzifer.io/v1/badge?color=5d79b5&title=license&text=Apache+v2.0)](http://www.apache.org/licenses/LICENSE-2.0) [![Gobuild Download](https://badge.luzifer.io/v1/badge?color=5d79b5&title=Download&text=on+GoBuilder.me)](https://gobuilder.me/github.com/Luzifer/s3sync)
|
||||
|
||||
`s3sync` is a small utility to sync local directories from/to Amazon S3 without installing any dependencies. Just put the binary somewhere into your path and set three ENV variables and your're ready to sync.
|
||||
|
||||
## Features
|
||||
|
||||
- Static binary, no dependencies required
|
||||
- Sync files only if required (judged by MD5 checksum)
|
||||
- Optionally delete files at target
|
||||
- Optionally make files public on sync (only if file needs sync)
|
||||
- Sync local-to-s3, s3-to-local, local-to-local or s3-to-s3
|
||||
|
||||
## Usage
|
||||
|
||||
1. Set `AWS_ACCESS_KEY`, `AWS_SECRET_ACCESS_KEY` and `AWS_REGION`
|
||||
2. Execute your sync
|
||||
|
||||
```bash
|
||||
# s3sync help
|
||||
Sync files from <from> to <to>
|
||||
|
||||
Usage:
|
||||
s3sync <from> <to> [flags]
|
||||
s3sync [command]
|
||||
|
||||
Available Commands:
|
||||
version Returns the current version of s3sync
|
||||
help Help about any command
|
||||
|
||||
Flags:
|
||||
-d, --delete=false: Delete files on remote not existing on local
|
||||
-h, --help=false: help for s3sync
|
||||
-P, --public=false: Make files public when syncing to S3
|
||||
|
||||
|
||||
Use "s3sync [command] --help" for more information about a command.
|
||||
|
||||
# s3sync -d 1/ s3://knut-test-s3sync/
|
||||
(1 / 3) 05/11/pwd_luzifer_io.png OK
|
||||
(2 / 3) 07/26/bkm.png OK
|
||||
(3 / 3) 07/26/luzifer_io.png OK
|
||||
|
||||
# s3sync -d 1/ s3://knut-test-s3sync/
|
||||
(1 / 3) 05/11/pwd_luzifer_io.png Skip
|
||||
(2 / 3) 07/26/bkm.png Skip
|
||||
(3 / 3) 07/26/luzifer_io.png Skip
|
||||
|
||||
# rm -rf 1/05
|
||||
# s3sync -d s3://knut-test-s3sync/ 1/
|
||||
(1 / 3) 05/11/pwd_luzifer_io.png OK
|
||||
(2 / 3) 07/26/bkm.png Skip
|
||||
(3 / 3) 07/26/luzifer_io.png Skip
|
||||
```
|
68
local.go
Normal file
68
local.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type localProvider struct{}
|
||||
|
||||
func newLocalProvider() *localProvider {
|
||||
return &localProvider{}
|
||||
}
|
||||
|
||||
func (l *localProvider) WriteFile(path string, content io.ReadSeeker, public bool) error {
|
||||
os.MkdirAll(filepath.Dir(path), 0755)
|
||||
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(f, content); err != nil {
|
||||
return err
|
||||
}
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func (l *localProvider) ReadFile(path string) (io.ReadCloser, error) {
|
||||
return os.Open(path)
|
||||
}
|
||||
|
||||
func (l *localProvider) ListFiles(prefix string) ([]file, error) {
|
||||
out := []file{}
|
||||
|
||||
err := filepath.Walk(prefix, func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !f.IsDir() {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
out = append(out, file{
|
||||
Filename: strings.TrimLeft(strings.Replace(path, prefix, "", 1), "/"),
|
||||
Size: f.Size(),
|
||||
MD5: fmt.Sprintf("%x", md5.Sum(content)),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return out, err
|
||||
}
|
||||
|
||||
func (l *localProvider) DeleteFile(path string) error {
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func (l *localProvider) GetAbsolutePath(path string) (string, error) {
|
||||
return filepath.Abs(path)
|
||||
}
|
148
main.go
Normal file
148
main.go
Normal file
|
@ -0,0 +1,148 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
cfg = struct {
|
||||
Delete bool
|
||||
Public bool
|
||||
}{}
|
||||
version = "dev"
|
||||
)
|
||||
|
||||
type file struct {
|
||||
Filename string
|
||||
Size int64
|
||||
MD5 string
|
||||
}
|
||||
|
||||
type filesystemProvider interface {
|
||||
WriteFile(path string, content io.ReadSeeker, public bool) error
|
||||
ReadFile(path string) (io.ReadCloser, error)
|
||||
ListFiles(prefix string) ([]file, error)
|
||||
DeleteFile(path string) error
|
||||
GetAbsolutePath(path string) (string, error)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cobra.Command{
|
||||
Use: "s3sync <from> <to>",
|
||||
Short: "Sync files from <from> to <to>",
|
||||
Run: execSync,
|
||||
}
|
||||
|
||||
app.AddCommand(&cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Returns the current version of s3sync",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Printf("s3sync %s\n", version)
|
||||
},
|
||||
})
|
||||
|
||||
app.Flags().BoolVarP(&cfg.Public, "public", "P", false, "Make files public when syncing to S3")
|
||||
app.Flags().BoolVarP(&cfg.Delete, "delete", "d", false, "Delete files on remote not existing on local")
|
||||
|
||||
app.Execute()
|
||||
}
|
||||
|
||||
func execSync(cmd *cobra.Command, args []string) {
|
||||
if len(args) != 2 {
|
||||
cmd.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
local, err := getFSProvider(args[0])
|
||||
errExit(err)
|
||||
remote, err := getFSProvider(args[1])
|
||||
errExit(err)
|
||||
|
||||
localPath, err := local.GetAbsolutePath(args[0])
|
||||
errExit(err)
|
||||
remotePath, err := remote.GetAbsolutePath(args[1])
|
||||
errExit(err)
|
||||
|
||||
localFiles, err := local.ListFiles(localPath)
|
||||
errExit(err)
|
||||
remoteFiles, err := remote.ListFiles(remotePath)
|
||||
errExit(err)
|
||||
|
||||
for i, localFile := range localFiles {
|
||||
fmt.Printf("(%d / %d) %s ", i+1, len(localFiles), localFile.Filename)
|
||||
needsCopy := true
|
||||
for _, remoteFile := range remoteFiles {
|
||||
if remoteFile.Filename == localFile.Filename && remoteFile.MD5 == localFile.MD5 {
|
||||
needsCopy = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if needsCopy {
|
||||
l, err := local.ReadFile(path.Join(localPath, localFile.Filename))
|
||||
if err != nil {
|
||||
fmt.Printf("ERR: %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
buffer, err := ioutil.ReadAll(l)
|
||||
if err != nil {
|
||||
fmt.Printf("ERR: %s\n", err)
|
||||
continue
|
||||
}
|
||||
l.Close()
|
||||
|
||||
err = remote.WriteFile(path.Join(remotePath, localFile.Filename), bytes.NewReader(buffer), cfg.Public)
|
||||
if err != nil {
|
||||
fmt.Printf("ERR: %s\n", err)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("OK\n")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("Skip\n")
|
||||
}
|
||||
|
||||
if cfg.Delete {
|
||||
for _, remoteFile := range remoteFiles {
|
||||
needsDeletion := true
|
||||
for _, localFile := range localFiles {
|
||||
if localFile.Filename == remoteFile.Filename {
|
||||
needsDeletion = false
|
||||
}
|
||||
}
|
||||
|
||||
if needsDeletion {
|
||||
fmt.Printf("delete: %s ", remoteFile.Filename)
|
||||
if err := remote.DeleteFile(path.Join(remotePath, remoteFile.Filename)); err != nil {
|
||||
fmt.Printf("ERR: %s\n", err)
|
||||
continue
|
||||
}
|
||||
fmt.Printf("OK\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func errExit(err error) {
|
||||
if err != nil {
|
||||
fmt.Printf("ERR: %s\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getFSProvider(prefix string) (filesystemProvider, error) {
|
||||
if strings.HasPrefix(prefix, "s3://") {
|
||||
return newS3Provider()
|
||||
}
|
||||
return newLocalProvider(), nil
|
||||
}
|
135
s3.go
Normal file
135
s3.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
)
|
||||
|
||||
type s3Provider struct {
|
||||
conn *s3.S3
|
||||
}
|
||||
|
||||
func newS3Provider() (*s3Provider, error) {
|
||||
return &s3Provider{
|
||||
conn: s3.New(&aws.Config{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *s3Provider) getBucketPath(prefix string) (bucket string, path string, err error) {
|
||||
rex := regexp.MustCompile(`^s3://?([^/]+)/(.*)$`)
|
||||
matches := rex.FindStringSubmatch(prefix)
|
||||
if len(matches) != 3 {
|
||||
err = fmt.Errorf("prefix did not match requirements")
|
||||
return
|
||||
}
|
||||
|
||||
bucket = matches[1]
|
||||
path = matches[2]
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *s3Provider) ListFiles(prefix string) ([]file, error) {
|
||||
out := []file{}
|
||||
|
||||
bucket, path, err := s.getBucketPath(prefix)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
in := &s3.ListObjectsInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Marker: nil,
|
||||
Prefix: aws.String(path),
|
||||
}
|
||||
for {
|
||||
o, err := s.conn.ListObjects(in)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
|
||||
for _, v := range o.Contents {
|
||||
out = append(out, file{
|
||||
Filename: *v.Key,
|
||||
Size: *v.Size,
|
||||
MD5: strings.Trim(*v.ETag, "\""), // Wat?
|
||||
})
|
||||
}
|
||||
|
||||
if o.NextMarker == nil {
|
||||
break
|
||||
}
|
||||
in.Marker = o.NextMarker
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *s3Provider) WriteFile(path string, content io.ReadSeeker, public bool) error {
|
||||
bucket, path, err := s.getBucketPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ext := filepath.Ext(path)
|
||||
mimeType := mime.TypeByExtension(ext)
|
||||
if mimeType == "" {
|
||||
mimeType = "application/octet-stream"
|
||||
}
|
||||
|
||||
params := &s3.PutObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(path),
|
||||
Body: content,
|
||||
ContentType: aws.String(mimeType),
|
||||
}
|
||||
if public {
|
||||
params.ACL = aws.String("public-read")
|
||||
}
|
||||
_, err = s.conn.PutObject(params)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *s3Provider) ReadFile(path string) (io.ReadCloser, error) {
|
||||
bucket, path, err := s.getBucketPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
o, err := s.conn.GetObject(&s3.GetObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(path),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return o.Body, nil
|
||||
}
|
||||
|
||||
func (s *s3Provider) DeleteFile(path string) error {
|
||||
bucket, path, err := s.getBucketPath(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.conn.DeleteObject(&s3.DeleteObjectInput{
|
||||
Bucket: aws.String(bucket),
|
||||
Key: aws.String(path),
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *s3Provider) GetAbsolutePath(path string) (string, error) {
|
||||
return path, nil
|
||||
}
|
Loading…
Reference in a new issue