Skip to content

Commit 16f4ff6

Browse files
committed
Initial commit
1 parent e2b5d8a commit 16f4ff6

File tree

2 files changed

+211
-1
lines changed

2 files changed

+211
-1
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,19 @@
11
s3-website-sync
22
===============
33

4-
Will sync a static website up to S3
4+
Will sync a static website up to S3. Compresses HTML and CSS using gzip before
5+
uploading. Does not compress images. Compares destination files to source files
6+
to avoid uploading files when they contain the same exact data (this is why its
7+
a sync not a copy).
8+
9+
To build this you'll need to:
10+
11+
go get github.com/mitchellh/goamz/aws
12+
go get github.com/mitchellh/goamz/s3
13+
14+
15+
# Usage
16+
17+
Usage of ./s3-website-sync:
18+
-bucket="": The name of the destination bucket in S3
19+
-source-path="": The path to the source directory containing the website

s3-website-sync.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package main
2+
3+
import (
4+
"compress/gzip"
5+
"crypto/md5"
6+
"flag"
7+
"fmt"
8+
"github.com/mitchellh/goamz/aws"
9+
"github.com/mitchellh/goamz/s3"
10+
"io"
11+
"io/ioutil"
12+
"log"
13+
"os"
14+
"strings"
15+
)
16+
17+
var content_type_map = map[string]string {
18+
"css": "text/css",
19+
"html": "text/html",
20+
"js": "text/javascript",
21+
}
22+
23+
func get_s3_dir(bucket *s3.Bucket, directory_path string, s3_contents *map[string]s3.Key) {
24+
contents, err := bucket.List(directory_path, "/", "/", 1024)
25+
if err != nil {
26+
log.Print("no listing?", err)
27+
os.Exit(1)
28+
}
29+
for _, key := range contents.Contents {
30+
(*s3_contents)[key.Key] = key
31+
}
32+
for _, subdir := range contents.CommonPrefixes {
33+
get_s3_dir(bucket, subdir, s3_contents)
34+
}
35+
}
36+
37+
type FileInfo struct {
38+
absolute_path string
39+
compressed_path string
40+
os_fileinfo os.FileInfo
41+
}
42+
43+
func main() {
44+
45+
var source_path = flag.String("source-path", "", "Source directory")
46+
var dest_bucket = flag.String("bucket", "", "Bucket in S3")
47+
flag.Parse()
48+
if *source_path == "" || *dest_bucket == "" {
49+
log.Println("Missing -source-path or -bucket")
50+
flag.Usage()
51+
os.Exit(1)
52+
}
53+
creds, err := aws.EnvAuth()
54+
if err != nil {
55+
log.Println("I messed up:", err)
56+
os.Exit(1)
57+
}
58+
59+
region := aws.Regions["us-east-1"]
60+
s3_conn := s3.New(creds, region)
61+
bucket := s3_conn.Bucket(*dest_bucket)
62+
if bucket == nil {
63+
log.Println("no bucket?")
64+
os.Exit(1)
65+
}
66+
67+
s3_keys := map[string]s3.Key {}
68+
get_s3_dir(bucket, "", &s3_keys)
69+
70+
all_files := make(chan *FileInfo, 10)
71+
go get_all_files(*source_path, all_files, true)
72+
process_all_files(*source_path, all_files, bucket, s3_keys)
73+
}
74+
75+
func hash_file(filename string) (string, error) {
76+
file, err := os.Open(filename)
77+
if err != nil {
78+
log.Println("Could not open %v: %v", filename, err)
79+
return "", err
80+
}
81+
defer file.Close()
82+
83+
hasher := md5.New()
84+
io.Copy(hasher,file)
85+
hash_val := fmt.Sprintf("%x", hasher.Sum(nil))
86+
return hash_val, nil
87+
}
88+
89+
func process_all_files(source_path string, all_files chan *FileInfo, bucket *s3.Bucket, s3_keys map[string]s3.Key) {
90+
for file_info := range all_files {
91+
if file_info == nil {
92+
break
93+
}
94+
log.Println(file_info.absolute_path)
95+
96+
headers := map[string][]string {}
97+
var cache_control []string
98+
cache_control = append(cache_control, "max-age=900")
99+
headers["Cache-Control"] = cache_control
100+
101+
dot_idx := 1 + strings.LastIndex(file_info.absolute_path, ".")
102+
suffix := file_info.absolute_path[dot_idx:]
103+
if suffix != "jpg" && suffix != "gif" && suffix != "png" {
104+
log.Println("\tcompressing", suffix)
105+
compressed_file, err := ioutil.TempFile("", "s3uploader")
106+
if err != nil {
107+
log.Println("\tCouldn't get a temp file", err)
108+
continue
109+
}
110+
gzipper, _ := gzip.NewWriterLevel(compressed_file, gzip.BestCompression)
111+
file, err := os.Open(file_info.absolute_path)
112+
if err != nil {
113+
log.Println("\tCouldn't open original file", file_info.absolute_path, err)
114+
continue
115+
}
116+
io.Copy(gzipper, file)
117+
file.Close()
118+
gzipper.Close()
119+
file_info.compressed_path = compressed_file.Name()
120+
121+
var content_encoding []string
122+
content_encoding = append(content_encoding, "gzip")
123+
var content_type []string
124+
content_type_str, ok := content_type_map[suffix]
125+
if !ok {
126+
content_type_str = "application/octet-stream"
127+
log.Println("\tUnknown extension:", file_info.absolute_path)
128+
}
129+
content_type = append(content_type, content_type_str)
130+
headers["Content-Type"] = content_type
131+
headers["Content-Encoding"] = content_encoding
132+
}
133+
134+
var path_to_contents string
135+
if len(file_info.compressed_path) > 0 {
136+
path_to_contents = file_info.compressed_path
137+
} else {
138+
path_to_contents = file_info.absolute_path
139+
}
140+
hash, err := hash_file(path_to_contents)
141+
if err != nil {
142+
log.Printf("\tCouldn't do hash for %s: %v\n",
143+
file_info.absolute_path, err)
144+
}
145+
key_name := file_info.absolute_path[len(source_path) + 1:]
146+
key, ok := s3_keys[key_name]
147+
// this library returns the ETag with quotes around it, we strip them
148+
if ok && key.ETag[1:len(key.ETag)-1] == hash {
149+
log.Println("\thashes match, no upload required", key_name)
150+
continue
151+
} else {
152+
log.Println("\tUploading", key_name)
153+
}
154+
155+
info, _ := os.Stat(path_to_contents)
156+
file, err := os.Open(path_to_contents)
157+
if err != nil {
158+
log.Printf("Can't open file %s: %v\n", path_to_contents, err)
159+
}
160+
bucket.PutReaderHeader(key_name, file, info.Size(),
161+
headers, s3.PublicRead)
162+
file.Close()
163+
if len(file_info.compressed_path) > 0 {
164+
os.Remove(file_info.compressed_path)
165+
}
166+
log.Println("\tFinished upload")
167+
168+
169+
}
170+
}
171+
172+
func get_all_files(dirname string, all_files chan *FileInfo, first_call bool) {
173+
file, err := os.Open(dirname)
174+
if err != nil {
175+
log.Fatalf("Could not cd to %s (%v), aborting.\n", dirname, err)
176+
}
177+
fileinfos, err := file.Readdir(0)
178+
if err != nil {
179+
log.Fatal("Couldn't read dir")
180+
}
181+
for _, fileinfo := range fileinfos {
182+
full_path := fmt.Sprintf("%s/%s", dirname, fileinfo.Name())
183+
if fileinfo.IsDir() {
184+
get_all_files(full_path, all_files, false)
185+
} else {
186+
this_file := new(FileInfo)
187+
this_file.absolute_path = full_path
188+
this_file.os_fileinfo = fileinfo
189+
all_files <- this_file
190+
}
191+
}
192+
if first_call {
193+
all_files <- nil
194+
}
195+
}

0 commit comments

Comments
 (0)