├── .gitignore ├── Gomfile ├── upd.service ├── Dockerfile ├── src ├── server │ ├── cors.go │ ├── metadata.go │ ├── auth_check.go │ ├── clean_job.go │ ├── config.go │ ├── last_uploaded.go │ ├── delete_handler.go │ ├── search_tags.go │ ├── serving_handler.go │ ├── send_handler.go │ ├── file.go │ └── server.go └── client │ ├── flags.go │ ├── search_tags.go │ ├── client.go │ └── send.go ├── PKGBUILD ├── server.conf.sample ├── bin ├── server │ └── server.go └── client │ └── client.go ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | *.swp 3 | certs/ 4 | -------------------------------------------------------------------------------- /Gomfile: -------------------------------------------------------------------------------- 1 | gom "github.com/gorilla/mux" 2 | gom "github.com/BurntSushi/toml" 3 | gom "github.com/vaughan0/go-ini" 4 | gom "github.com/aws/aws-sdk-go" 5 | gom "github.com/nfnt/resize" 6 | gom "github.com/boltdb/bolt" 7 | gom "github.com/go-ini/ini" 8 | gom "github.com/jmespath/go-jmespath" 9 | -------------------------------------------------------------------------------- /upd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=upd 3 | After=docker.service 4 | Requires=docker.service 5 | 6 | [Service] 7 | TimeoutStartSec=0 8 | ExecStart=/usr/bin/docker run --rm -p 9000:9000 -v /home/user:/etc/upd -v /home/user/data:/tmp upd 9 | ExecStop=/usr/bin/docker kill upd 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.5 2 | 3 | COPY . /go/src/github.com/remeh/upd 4 | 5 | RUN go get github.com/mattn/gom \ 6 | && cd /go/src/github.com/remeh/upd \ 7 | && gom install \ 8 | && gom build bin/server/server.go 9 | 10 | EXPOSE 9000 11 | 12 | ENTRYPOINT ["/go/src/github.com/remeh/upd/server"] 13 | CMD ["-c", "/etc/upd/server.conf"] 14 | -------------------------------------------------------------------------------- /src/server/cors.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "net/http" 4 | 5 | // CorsHandler adds the required CORS headers, and forwards the request to the real handler 6 | // (with the notable exception of OPTIONS requests, that it will eat) 7 | type CorsHandler struct { 8 | h http.Handler 9 | } 10 | 11 | func (c *CorsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 12 | headers := w.Header() 13 | headers.Set("Allow", "GET, POST, OPTIONS") 14 | headers.Set("Access-Control-Allow-Headers", "Content-Type, Accept, X-upd-key") 15 | headers.Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") 16 | headers.Set("Access-Control-Allow-Origin", "*") 17 | 18 | if r.Method == "OPTIONS" { 19 | w.WriteHeader(200) 20 | } else { 21 | c.h.ServeHTTP(w, r) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/server/metadata.go: -------------------------------------------------------------------------------- 1 | // Saving information on the hosted files. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | package server 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | type Metadata struct { 10 | Original string `json:"original"` // original name of the file. 11 | Filename string `json:"filename"` // name of the file on the FS 12 | Tags []string `json:"tags"` // tags attached to the uploaded file 13 | TTL string `json:"ttl"` // time.Duration representing the lifetime of the file. 14 | ExpirationTime time.Time `json:"expiration_time"` // at which time this file should expire. 15 | DeleteKey string `json:"delete_key"` // The key to delete this file. 16 | CreationTime time.Time `json:"creation_time"` 17 | } 18 | -------------------------------------------------------------------------------- /src/client/flags.go: -------------------------------------------------------------------------------- 1 | // Client flags. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package client 5 | 6 | import "strings" 7 | 8 | type Tags []string 9 | 10 | func (t *Tags) Set(s string) error { 11 | *t = append(*t, strings.TrimSpace(s)) 12 | return nil 13 | } 14 | 15 | func (t Tags) String() string { 16 | return strings.Join(t, ",") 17 | } 18 | 19 | // Flags for client configuration 20 | type Flags struct { 21 | ServerUrl string // Address to send to 22 | SecretKey string // Secret between the client and the server 23 | TTL string // when a ttl is given for a file 24 | CA string // Should we use HTTPS, and in which config "none", file to a CA or "unsafe" 25 | SearchTags string // if we wanna look for some files by tags 26 | 27 | Tags Tags // Array of tag to attach to the file 28 | } 29 | -------------------------------------------------------------------------------- /PKGBUILD: -------------------------------------------------------------------------------- 1 | # Maintainer: Rémy Mathieu 2 | pkgname=upd-git 3 | pkgver=1.0 4 | pkgrel=1 5 | pkgdesc="File sharing daemon/client". 6 | arch=('i686' 'x86_64') 7 | url="https://github.com/remeh/upd" 8 | license=('Apache 2.0') 9 | groups=() 10 | depends=() 11 | makedepends=('go' 'git') 12 | install= 13 | source=("git://github.com/remeh/upd.git") 14 | md5sums=('SKIP') 15 | 16 | build() { 17 | export GOPATH=$srcdir 18 | go get github.com/mattn/gom 19 | cd ${srcdir}/upd 20 | git checkout 1.0 21 | ${srcdir}/bin/gom install 22 | ${srcdir}/bin/gom build bin/server/server.go 23 | ${srcdir}/bin/gom build bin/client/client.go 24 | } 25 | 26 | package() { 27 | cd $_gitname 28 | install -Dm755 "${srcdir}/upd/server" "$pkgdir/usr/bin/upd-server" 29 | install -Dm755 "${srcdir}/upd/client" "$pkgdir/usr/bin/upd-client" 30 | } 31 | -------------------------------------------------------------------------------- /src/server/auth_check.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "net/http" 4 | 5 | // AuthCheckHandler simply tests whether the presented auth credentials are valid or not 6 | // without doing any useless work 7 | type AuthCheckHandler struct { 8 | Server *Server // pointer to the started server 9 | } 10 | 11 | func (a *AuthCheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 12 | if !IsAuthValid(a.Server, r) { 13 | w.WriteHeader(403) 14 | w.Write([]byte(`{"auth_status":"invalid_credentials"}`)) 15 | return 16 | } 17 | 18 | w.Write([]byte(`{"auth_status":"ok"}`)) 19 | } 20 | 21 | // IsAuthValid returns whether the HTTP request contains the expected secret key, if the configuration 22 | // requires one 23 | func IsAuthValid(s *Server, r *http.Request) bool { 24 | key := r.Header.Get(SECRET_KEY_HEADER) 25 | return s.Config.SecretKey == "" || key == s.Config.SecretKey 26 | } 27 | -------------------------------------------------------------------------------- /server.conf.sample: -------------------------------------------------------------------------------- 1 | # 2 | # upd configuration sample 3 | # ----------------------- 4 | 5 | # The address to listen to with the server. 6 | listen_addr = ":9000" 7 | 8 | # The secret key to identify the client. (optional) 9 | secret_key = "" 10 | 11 | # Route served by the server. 12 | route = "/upd" 13 | 14 | # Path to a tls certificate file. Ex: /usr/share/certs/upd.pem (optional) 15 | certificate = "" 16 | 17 | # Path to a tls certificate key. Ex: /usr/share/certs/key.pem (optional) 18 | certificate_key = "" 19 | 20 | # Directory in which the server can write the runtime files. 21 | runtime_dir = "/tmp" 22 | 23 | # 24 | # Storage configuration 25 | # 26 | 27 | # Which storage you want to use to store the files 28 | # Possible values: fs, s3 29 | storage = "fs" 30 | 31 | # 32 | # Filesystem storage configuration 33 | # 34 | [fsstorage] 35 | 36 | # If you chose 'fs' as a storage, you must provide an 37 | # output directory 38 | output_dir = "/tmp" 39 | 40 | 41 | # 42 | # S3 storage configuration 43 | # 44 | [s3storage] 45 | 46 | access_key = "" 47 | access_secret = "" 48 | region = "" # example 'eu-west-1' 49 | bucket = "" 50 | -------------------------------------------------------------------------------- /src/server/clean_job.go: -------------------------------------------------------------------------------- 1 | // Job launched and regularly executed to clean 2 | // the expired files. 3 | // Copyright © 2015 - Rémy MATHIEU 4 | 5 | package server 6 | 7 | import ( 8 | "encoding/json" 9 | "log" 10 | "time" 11 | 12 | "github.com/boltdb/bolt" 13 | ) 14 | 15 | type CleanJob struct { 16 | server *Server 17 | } 18 | 19 | // Run deals with cleaning the expired files by 20 | // checking their TTL. 21 | func (j CleanJob) Run() { 22 | var entries []Metadata 23 | 24 | j.server.Database.View(func(tx *bolt.Tx) error { 25 | b := tx.Bucket([]byte("Metadata")) 26 | c := b.Cursor() 27 | 28 | for k, v := c.First(); k != nil; k, v = c.Next() { 29 | // unmarshal 30 | var entry Metadata 31 | err := json.Unmarshal(v, &entry) 32 | if err != nil { 33 | log.Println("[err] Can't read a metadata:", err.Error()) 34 | continue 35 | } 36 | 37 | if !entry.ExpirationTime.IsZero() && entry.ExpirationTime.Before(time.Now()) { 38 | entries = append(entries, entry) 39 | } 40 | } 41 | 42 | return nil 43 | }) 44 | 45 | for _, entry := range entries { 46 | // No longer alive! 47 | err := j.server.Expire(entry) 48 | if err != nil { 49 | log.Println("[warn] While deleting file:", entry.Filename) 50 | log.Println(err) 51 | } else { 52 | log.Println("[info] Deleted due to TTL:", entry.Filename) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/server/config.go: -------------------------------------------------------------------------------- 1 | // Server config. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package server 5 | 6 | // Server flags 7 | type Flags struct { 8 | ConfigFile string // the file configuration to use for the server 9 | } 10 | 11 | const ( 12 | FS_STORAGE = "fs" 13 | S3_STORAGE = "s3" 14 | ) 15 | 16 | // Server configuration 17 | type Config struct { 18 | Addr string `toml:"listen_addr"` // Address to listen to 19 | SecretKey string `toml:"secret_key"` // Secret between the client and the server 20 | RuntimeDir string `toml:"runtime_dir"` // Where the server can write the runtime files. 21 | Route string `toml:"route"` // Route served by the webserver 22 | CertificateFile string `toml:"certificate"` // Filepath to an tls certificate 23 | CertificateKey string `toml:"certificate_key"` // Filepath to the key part of a certificate 24 | 25 | Storage string `toml:"storage"` // possible values 'fs', 's3' 26 | 27 | FSConfig FSConfig `toml:"fsstorage"` 28 | S3Config S3Config `toml:"s3storage"` 29 | } 30 | 31 | type FSConfig struct { 32 | OutputDirectory string `toml:"output_dir"` 33 | } 34 | 35 | type S3Config struct { 36 | AccessKey string `toml:"access_key"` 37 | AccessSecret string `toml:"access_secret"` 38 | Region string `toml:"region"` 39 | Bucket string `toml:"bucket"` 40 | } 41 | -------------------------------------------------------------------------------- /src/server/last_uploaded.go: -------------------------------------------------------------------------------- 1 | // Route giving the last uploaded files. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | package server 4 | 5 | import ( 6 | "encoding/json" 7 | "log" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | const ( 13 | MAX_LAST_UPLOADED = 20 14 | ) 15 | 16 | type LastUploadedHandler struct { 17 | Server *Server // pointer to the started server 18 | } 19 | 20 | // Json returned to the client 21 | type LastUploadedResponse struct { 22 | Name string `json:"name"` 23 | Original string `json:"original"` 24 | DeleteKey string `json:"delete_key"` 25 | CreationTime time.Time `json:"creation_time"` 26 | } 27 | 28 | func (l *LastUploadedHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 29 | if !IsAuthValid(l.Server, r) { 30 | w.WriteHeader(403) 31 | return 32 | } 33 | 34 | lastUploadedResp := make([]LastUploadedResponse, 0) 35 | lastUploaded, err := l.Server.GetLastUploaded() 36 | if err != nil { 37 | log.Println("[err] can't retrieve the last uploaded ids:", err.Error()) 38 | w.WriteHeader(500) 39 | return 40 | } 41 | 42 | for _, id := range lastUploaded { 43 | metadata, err := l.Server.GetEntry(id) 44 | if err != nil { 45 | log.Println("[err] Error while retrieving an entry in LastUploaded handler:", err.Error()) 46 | continue 47 | } 48 | 49 | if metadata == nil { 50 | continue 51 | } 52 | 53 | lastUploadedResp = append(lastUploadedResp, LastUploadedResponse{ 54 | Name: metadata.Filename, 55 | Original: metadata.Original, 56 | DeleteKey: metadata.DeleteKey, 57 | CreationTime: metadata.CreationTime, 58 | }) 59 | } 60 | 61 | bytes, err := json.Marshal(lastUploadedResp) 62 | if err != nil { 63 | log.Println("[err] Can't marshal the list of last uploaded:", err.Error()) 64 | w.WriteHeader(500) 65 | } 66 | 67 | w.Write(bytes) 68 | } 69 | -------------------------------------------------------------------------------- /src/server/delete_handler.go: -------------------------------------------------------------------------------- 1 | // Route to delete a file from the server 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package server 5 | 6 | import ( 7 | "log" 8 | "net/http" 9 | 10 | "github.com/gorilla/mux" 11 | ) 12 | 13 | type DeleteHandler struct { 14 | Server *Server // pointer to the started server 15 | } 16 | 17 | func (s *DeleteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | // Parse the route parameters 19 | vars := mux.Vars(r) 20 | 21 | id := vars["file"] // file id 22 | key := vars["key"] // delete key 23 | 24 | if len(id) == 0 || len(key) == 0 { 25 | w.WriteHeader(400) 26 | return 27 | } 28 | 29 | // Existing file ? 30 | entry, err := s.Server.GetEntry(id) 31 | if err != nil { 32 | log.Println("Can't use the database:", err.Error()) 33 | w.WriteHeader(500) 34 | return 35 | } 36 | if entry == nil { 37 | w.WriteHeader(404) 38 | return 39 | } 40 | 41 | // checks that the key is correct 42 | if entry.DeleteKey != key { 43 | w.WriteHeader(403) 44 | return 45 | } 46 | 47 | // deletes the file 48 | err = s.Server.Expire(*entry) 49 | if err != nil { 50 | log.Println("[err] While deleting the entry:", entry.Filename) 51 | log.Println(err) 52 | w.WriteHeader(500) 53 | return 54 | } 55 | 56 | // we must remove the line from the LastUploaded 57 | lastUploaded, err := s.Server.GetLastUploaded() 58 | if err != nil { 59 | log.Println("[err] Can't retrieve the last uploaded when deleting.") 60 | w.WriteHeader(500) 61 | return 62 | } 63 | 64 | lastUploaded = s.removeFromLastUploaded(lastUploaded, id) 65 | 66 | s.Server.SetLastUploaded(lastUploaded) 67 | 68 | w.WriteHeader(200) 69 | w.Write([]byte("File deleted.")) 70 | } 71 | 72 | func (s *DeleteHandler) removeFromLastUploaded(lastUploaded []string, id string) []string { 73 | result := make([]string, 0) 74 | 75 | for _, lu := range lastUploaded { 76 | if lu == id { 77 | continue 78 | } 79 | result = append(result, lu) 80 | } 81 | 82 | return result 83 | } 84 | -------------------------------------------------------------------------------- /bin/server/server.go: -------------------------------------------------------------------------------- 1 | // Server executable to receive/host files. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | 12 | "server" 13 | 14 | "github.com/BurntSushi/toml" 15 | ) 16 | 17 | // readFromFile looks whether a configuration file is provided 18 | // to read the config into it. 19 | func readFromFile(filename string) (server.Config, error) { 20 | // default hardcoded config when no configuration file is available 21 | config := server.Config{ 22 | Addr: ":9000", 23 | Storage: "fs", 24 | RuntimeDir: "/tmp", 25 | FSConfig: server.FSConfig{ 26 | OutputDirectory: "/tmp", 27 | }, 28 | Route: "/upd", 29 | } 30 | 31 | // read the file 32 | data, err := ioutil.ReadFile(filename) 33 | if err != nil { 34 | return config, err 35 | } 36 | 37 | // decode the file 38 | _, err = toml.Decode(string(data), &config) 39 | if err != nil { 40 | return config, err 41 | } 42 | 43 | // Ensure the validity of the config // TODO log 44 | if config.Route[0] != '/' { 45 | config.Route = "/" + config.Route 46 | } 47 | if config.Route[len(config.Route)-1] == '/' { 48 | config.Route = config.Route[:len(config.Route)-1] 49 | } 50 | 51 | if config.Storage != server.FS_STORAGE && config.Storage != server.S3_STORAGE { 52 | log.Println("[err] Unknown storage:", config.Storage) 53 | os.Exit(1) 54 | } 55 | 56 | return config, nil 57 | } 58 | 59 | func parseFlags() server.Flags { 60 | var flags server.Flags 61 | 62 | // Declare the flags 63 | flag.StringVar(&(flags.ConfigFile), "c", "upd.conf", "Configuration file to use.") 64 | 65 | // Read them 66 | flag.Parse() 67 | 68 | return flags 69 | } 70 | 71 | func main() { 72 | flags := parseFlags() 73 | 74 | config, err := readFromFile(flags.ConfigFile) 75 | if err != nil { 76 | log.Println("[warn] Can't read the configuration file") 77 | log.Println("[warn] Falling back on default values for configuration.") 78 | } 79 | 80 | app := server.NewServer(config) 81 | app.Start() 82 | } 83 | -------------------------------------------------------------------------------- /src/client/search_tags.go: -------------------------------------------------------------------------------- 1 | // Client - Searching tags. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package client 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "net/http" 12 | "os" 13 | 14 | "server" 15 | ) 16 | 17 | const ( 18 | ROUTE_SEARCH_TAGS = "/1.0/search_tags" 19 | ) 20 | 21 | func (c *Client) SearchTags(tags []string) { 22 | // create the request 23 | client := c.createHttpClient() 24 | 25 | uri := c.Flags.ServerUrl + ROUTE_SEARCH_TAGS 26 | 27 | params := make(map[string]string) 28 | uri = c.buildParams(uri, params, tags) 29 | 30 | req, err := http.NewRequest("GET", uri, nil) 31 | 32 | // adds the secret key if any 33 | if len(c.Flags.SecretKey) > 0 { 34 | req.Header.Set(server.SECRET_KEY_HEADER, c.Flags.SecretKey) 35 | } 36 | 37 | // execute 38 | resp, err := client.Do(req) 39 | if err != nil { 40 | log.Println("[err] Unable to execute the request to search by tags:", err) 41 | os.Exit(1) 42 | } 43 | 44 | if resp.StatusCode != 200 { 45 | log.Printf("[err] Received a %d while searching by tags: %s", resp.StatusCode, tags) 46 | os.Exit(1) 47 | } 48 | 49 | // read the name given by the server 50 | defer resp.Body.Close() 51 | readBody, err := ioutil.ReadAll(resp.Body) 52 | if err != nil { 53 | log.Println("[err] Unable to read the body returned by the server:", err) 54 | os.Exit(1) 55 | } 56 | 57 | var entries server.SearchTagsResponse 58 | err = json.Unmarshal(readBody, &entries) 59 | if err != nil { 60 | log.Println("[err] Unable to read the response:", err) 61 | os.Exit(1) 62 | } 63 | 64 | for i := range entries.Results { 65 | entry := entries.Results[i] 66 | fmt.Printf("-> %s\n", entry.Original) 67 | fmt.Printf("Link: %s/%s\n", c.Flags.ServerUrl, entry.Filename) 68 | fmt.Printf("Deletion link: %s/%s/%s\n", c.Flags.ServerUrl, entry.Filename, entry.DeleteKey) 69 | fmt.Printf("Creation time: %s\n", entry.CreationTime) 70 | if !entry.ExpirationTime.IsZero() { 71 | fmt.Printf("Expiration time: %s\n", entry.ExpirationTime) 72 | } 73 | if len(entry.Tags) > 0 { 74 | fmt.Printf("Tags: %s\n", entry.Tags) 75 | } 76 | fmt.Println("-----------------------") 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/client/client.go: -------------------------------------------------------------------------------- 1 | // Client executable to send file to the upd daemon. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package client 5 | 6 | import ( 7 | "crypto/tls" 8 | "crypto/x509" 9 | "fmt" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | ) 15 | 16 | const ( 17 | ROUTE_SEND = "/1.0/send" 18 | ) 19 | 20 | type Client struct { 21 | Flags Flags 22 | } 23 | 24 | func NewClient(flags Flags) *Client { 25 | return &Client{Flags: flags} 26 | } 27 | 28 | // Send sends the given file to the upd server. 29 | func (c *Client) Send(filename string) error { 30 | // first, we need to read the data 31 | data, err := c.readFile(filename) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // and now to send it the servee 37 | return c.sendData(filename, data) 38 | } 39 | 40 | func (c *Client) createHttpClient() *http.Client { 41 | if c.Flags.CA == "unsafe" { 42 | tr := &http.Transport{ 43 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 44 | } 45 | return &http.Client{Transport: tr} 46 | } else if len(c.Flags.CA) > 0 && c.Flags.CA != "none" { 47 | // reads the CA 48 | certs := x509.NewCertPool() 49 | pemData, err := ioutil.ReadFile(c.Flags.CA) 50 | if err != nil { 51 | log.Println("[err] Can't read the CA.") 52 | } 53 | certs.AppendCertsFromPEM(pemData) 54 | tr := &http.Transport{ 55 | TLSClientConfig: &tls.Config{RootCAs: certs}, 56 | } 57 | return &http.Client{Transport: tr} 58 | } 59 | 60 | // No HTTPS support 61 | return &http.Client{} 62 | } 63 | 64 | // buildParams adds the GET parameters to the given uri. 65 | func (c *Client) buildParams(uri string, params map[string]string, tags []string) string { 66 | if len(params) == 0 && len(tags) == 0 { 67 | return uri 68 | } 69 | 70 | atLeastOne := false 71 | 72 | ret := uri 73 | ret += "?" 74 | 75 | for k, v := range params { 76 | if len(v) > 0 { 77 | ret = fmt.Sprintf("%s%s=%s&", ret, k, url.QueryEscape(v)) 78 | atLeastOne = true 79 | } 80 | } 81 | 82 | // remove last & if no tags 83 | if len(tags) == 0 { 84 | ret = ret[0 : len(ret)-1] 85 | } else { 86 | // add the tags. 87 | pTags := "tags=" 88 | for i := range tags { 89 | pTags = pTags + url.QueryEscape(tags[i]) 90 | if i < len(tags)-1 { 91 | pTags = pTags + url.QueryEscape(",") 92 | } 93 | atLeastOne = true 94 | } 95 | ret = ret + pTags 96 | } 97 | 98 | // there were parameters but they're all empty 99 | if !atLeastOne { 100 | return uri 101 | } 102 | 103 | return ret 104 | } 105 | -------------------------------------------------------------------------------- /bin/client/client.go: -------------------------------------------------------------------------------- 1 | // Client executable to upload data on a upd daemon. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package main 5 | 6 | import ( 7 | "flag" 8 | "fmt" 9 | "log" 10 | "os" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "client" 16 | ) 17 | 18 | type Client struct { 19 | } 20 | 21 | func parseFlags() (client.Flags, error) { 22 | var flags client.Flags 23 | 24 | // Declare the flags 25 | flag.StringVar(&(flags.CA), "ca", "none", "For HTTPS support: none / filename of an accepted CA / unsafe (doesn't check the CA)") 26 | flag.StringVar(&(flags.ServerUrl), "url", "http://localhost:9000/upd", "The server to contact") 27 | flag.StringVar(&(flags.SecretKey), "key", "", "A shared secret key to identify the client.") 28 | flag.StringVar(&(flags.TTL), "ttl", "", `TTL after which the file expires, ex: 30m. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"`) 29 | flag.StringVar(&(flags.SearchTags), "search-tags", "", "Search by tags. If many, must be separated by a comma, an 'or' operator is used. Ex: \"may,screenshot\".") 30 | flag.Var(&flags.Tags, "tags", "Tags to attach to the file, separated by a comma. Ex: \"screenshot,may\"") 31 | 32 | // Read them 33 | flag.Parse() 34 | 35 | // remove / on url if necessary 36 | if flags.ServerUrl[len(flags.ServerUrl)-1] == '/' { 37 | flags.ServerUrl = flags.ServerUrl[:len(flags.ServerUrl)-1] 38 | } 39 | 40 | // checks that the given ttl is correct 41 | if flags.TTL != "" { 42 | _, err := time.ParseDuration(flags.TTL) 43 | if err != nil { 44 | return flags, err 45 | } 46 | } 47 | 48 | return flags, nil 49 | } 50 | 51 | // sendFile uses the client to send the data to the upd server. 52 | func sendFile(wg *sync.WaitGroup, client *client.Client, filename string) { 53 | defer wg.Done() 54 | 55 | err := client.Send(filename) 56 | if err != nil { 57 | log.Println("[err] While sending:", filename) 58 | log.Println(err) 59 | } 60 | } 61 | 62 | func main() { 63 | flags, err := parseFlags() 64 | if err != nil { 65 | fmt.Println(`Wrong duration format, it should be such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h"`) 66 | os.Exit(1) 67 | } 68 | 69 | c := client.NewClient(flags) 70 | 71 | // Looks for tags to search 72 | if len(flags.SearchTags) > 0 { 73 | tags := strings.Split(flags.SearchTags, ",") 74 | for i := range tags { 75 | tags[i] = strings.Trim(tags[i], " ") 76 | } 77 | c.SearchTags(tags) 78 | } else { 79 | // Looks for the file to send 80 | // TODO directory 81 | if len(flag.Args()) < 1 { 82 | fmt.Printf("Usage: %s [flags] file1 file2\n", os.Args[0]) 83 | flag.PrintDefaults() 84 | } 85 | 86 | var wg sync.WaitGroup 87 | // Send each file. 88 | for _, filename := range flag.Args() { 89 | wg.Add(1) 90 | go sendFile(&wg, c, filename) 91 | } 92 | 93 | wg.Wait() // Wait for all routine to stop 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/server/search_tags.go: -------------------------------------------------------------------------------- 1 | // Route to search by tags 2 | // Copyright © 2015 - Rémy MATHIEU 3 | package server 4 | 5 | import ( 6 | "encoding/json" 7 | "log" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "github.com/boltdb/bolt" 13 | ) 14 | 15 | type SearchTagsHandler struct { 16 | Server *Server // pointer to the started server 17 | } 18 | 19 | // Json returned to the client 20 | type SearchTagsResponse struct { 21 | Results []SearchTagsEntryResponse `json:"results"` 22 | } 23 | 24 | // actually contains everything in Metadata but eh, 25 | // looks more clean to do so if oneee daaay... 26 | type SearchTagsEntryResponse struct { 27 | Filename string `json:"filename"` // name attributed by upd 28 | Original string `json:"original"` // original name of the file 29 | DeleteKey string `json:"delete_key"` // the delete key 30 | CreationTime time.Time `json:"creation_time"` // creation time of the given file 31 | ExpirationTime time.Time `json:"expiration_time"` // When this file expired 32 | Tags []string `json:"tags"` // Tags attached to this file. 33 | } 34 | 35 | func (l *SearchTagsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 36 | if !IsAuthValid(l.Server, r) { 37 | w.WriteHeader(403) 38 | return 39 | } 40 | 41 | r.ParseForm() 42 | if len(r.Form["tags"]) == 0 || len(r.Form["tags"][0]) == 0 { 43 | println("!") 44 | w.WriteHeader(400) 45 | return 46 | } 47 | 48 | tagParam := r.Form["tags"][0] 49 | tags := strings.Split(tagParam, ",") 50 | for i := range tags { 51 | tags[i] = strings.Trim(tags[i], " ") 52 | } 53 | 54 | // At the moment, without a 'database' system, we must 55 | // look in every entries to find if some have the tags. 56 | response := SearchTagsResponse{Results: make([]SearchTagsEntryResponse, 0)} 57 | 58 | l.Server.Database.View(func(tx *bolt.Tx) error { 59 | b := tx.Bucket([]byte("Metadata")) 60 | c := b.Cursor() 61 | 62 | for k, v := c.First(); k != nil; k, v = c.Next() { 63 | // unmarshal 64 | var metadata Metadata 65 | err := json.Unmarshal(v, &metadata) 66 | if err != nil { 67 | log.Println("[err] Can't read a metadata:", err.Error()) 68 | continue 69 | } 70 | 71 | if stringArrayContainsOne(metadata.Tags, tags) { 72 | entry := SearchTagsEntryResponse{ 73 | Filename: metadata.Filename, 74 | Original: metadata.Original, 75 | CreationTime: metadata.CreationTime, 76 | DeleteKey: metadata.DeleteKey, 77 | ExpirationTime: metadata.ExpirationTime, 78 | Tags: metadata.Tags, 79 | } 80 | response.Results = append(response.Results, entry) 81 | } 82 | } 83 | return nil 84 | }) 85 | 86 | bytes, err := json.Marshal(response) 87 | if err != nil { 88 | log.Println("[err] Can't marshal the list of last uploaded:", err.Error()) 89 | w.WriteHeader(500) 90 | } 91 | 92 | w.Write(bytes) 93 | } 94 | 95 | // stringArrayContains returns true if the array contains at least one of the values. 96 | func stringArrayContainsOne(array []string, tags []string) bool { 97 | for i := range array { 98 | for j := range tags { 99 | if array[i] == tags[j] { 100 | return true 101 | } 102 | } 103 | } 104 | return false 105 | } 106 | -------------------------------------------------------------------------------- /src/client/send.go: -------------------------------------------------------------------------------- 1 | // Client - Sending file. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package client 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "log" 13 | "mime/multipart" 14 | "net/http" 15 | "os" 16 | 17 | "server" 18 | ) 19 | 20 | // sendData sends the data to the upd server. 21 | func (c *Client) sendData(filename string, data []byte) error { 22 | // Prepare the multipart content 23 | body := &bytes.Buffer{} 24 | writer := multipart.NewWriter(body) 25 | 26 | part, err := writer.CreateFormFile("data", "file") 27 | if err != nil { 28 | log.Println("[err] Unable to prepare the multipart content (CreateFormFile)") 29 | return err 30 | } 31 | 32 | _, err = io.Copy(part, bytes.NewReader(data)) 33 | if err != nil { 34 | log.Println("[err] Unable to prepare the multipart content (Copy)") 35 | return err 36 | } 37 | 38 | err = writer.Close() 39 | if err != nil { 40 | log.Println("[err] Unable to prepare the multipart content (Close)") 41 | return err 42 | } 43 | 44 | // create the request 45 | client := c.createHttpClient() 46 | 47 | uri := c.Flags.ServerUrl + ROUTE_SEND 48 | 49 | params := make(map[string]string) 50 | params["ttl"] = c.Flags.TTL 51 | params["name"] = filename 52 | 53 | uri = c.buildParams(uri, params, c.Flags.Tags) 54 | 55 | req, err := http.NewRequest("POST", uri, body) 56 | req.Header.Add("Content-Type", writer.FormDataContentType()) 57 | if err != nil { 58 | log.Println("[err] Unable to create the request to send the file.") 59 | return err 60 | } 61 | 62 | // adds the secret key if any 63 | if len(c.Flags.SecretKey) > 0 { 64 | req.Header.Set(server.SECRET_KEY_HEADER, c.Flags.SecretKey) 65 | } 66 | 67 | // execute 68 | resp, err := client.Do(req) 69 | if err != nil { 70 | log.Println("[err] Unable to execut the request to send the file.") 71 | return err 72 | } 73 | 74 | if resp.StatusCode != 200 { 75 | return fmt.Errorf("[err] Received a %d while sending: %s", resp.StatusCode, filename) 76 | } 77 | 78 | // read the name given by the server 79 | defer resp.Body.Close() 80 | readBody, err := ioutil.ReadAll(resp.Body) 81 | if err != nil { 82 | log.Println("[err] Unable to read the body returned by the server.") 83 | return err 84 | } 85 | 86 | // decodes the json 87 | var sendResponse server.SendResponse 88 | err = json.Unmarshal(readBody, &sendResponse) 89 | if err != nil { 90 | log.Println("[err] Unable to read the returned JSON.") 91 | } 92 | 93 | fmt.Println("For file :", filename) 94 | fmt.Println("URL:", c.Flags.ServerUrl+"/"+sendResponse.Name) 95 | fmt.Println("Delete URL:", c.Flags.ServerUrl+"/"+sendResponse.Name+"/"+sendResponse.DeleteKey) 96 | 97 | // compute until when it'll be available 98 | if sendResponse.ExpirationTime.IsZero() { 99 | fmt.Println("Available forever.") 100 | } else { 101 | fmt.Println("Available until:", sendResponse.ExpirationTime) 102 | } 103 | fmt.Println("--") 104 | 105 | return nil 106 | } 107 | 108 | // readFile reads the content of the given file. 109 | func (c *Client) readFile(filename string) ([]byte, error) { 110 | result := make([]byte, 0) 111 | 112 | // opening 113 | file, err := os.Open(filename) 114 | if err != nil { 115 | return result, err 116 | } 117 | 118 | // reading 119 | result, err = ioutil.ReadAll(file) 120 | if err != nil { 121 | return result, err 122 | } 123 | 124 | return result, nil 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # upd 2 | 3 | Upload from CLI, share with browsers. 4 | 5 | ## About 6 | 7 | upd is a file upload service to quickly share files through http(s) supporting different storage backend (filesystem and Amazon S3). 8 | 9 | The server provides a simple API to easily allow the creation of clients and a command-line client is also provided in this repository. 10 | 11 | 12 | ## Features 13 | 14 | * Storages backend : Filesystem, Amazon S3 15 | * Daemon listening to receive files 16 | * Daemon serving files (with resize feature on images) 17 | * TTL for expiration of files. 18 | * Tags on files + search by tags API 19 | * Delete link 20 | * HTTPs 21 | * Secret shared key between client / server 22 | * Get last uploaded files 23 | * Routine job cleaning the expired files 24 | 25 | ## How to use 26 | 27 | ### Basics 28 | 29 | First, you need to use the excellent dependency system gom: 30 | 31 | ``` 32 | go get github.com/mattn/gom 33 | ``` 34 | 35 | Then, in the main directory of `upd` 36 | 37 | ``` 38 | gom install 39 | ``` 40 | 41 | to setup the dependencies. 42 | 43 | ### Start the daemon 44 | 45 | #### Normal server 46 | 47 | To start the server: 48 | 49 | ``` 50 | gom build bin/server/server.go 51 | ./server 52 | ``` 53 | 54 | Available flags for the `server` executable: 55 | 56 | ``` 57 | -c="server.conf": Path to a configuration file. 58 | ``` 59 | 60 | The configuration file is well-documented. 61 | 62 | #### Docker server 63 | 64 | upd daemon is ready to be launched with `Docker`. You must first build the docker container, in the upd directory : 65 | 66 | ``` 67 | docker build -t upd . 68 | ``` 69 | 70 | It'll build the upd server docker container. What you must know: 71 | * The `ENTRYPOINT` docker has been binded on the configuration file `/etc/upd/server.conf`, this way, by using a volume, you can provide your configuration file. 72 | * Don't forget to bind a volume for the data directory if you're using the filesystem storage backend. If you don't do so, you'll lost your data when the docker will be stopped/restarted. 73 | 74 | Example of how to launch the upd container (with the `server.conf` in your host `/home/user/`) : 75 | 76 | ``` 77 | docker run --rm -ti -v /home/user:/etc/upd -v /home/user/data:/tmp -p 9000:9000 upd 78 | ``` 79 | 80 | A `upd.service` is available in the repository as an example to use `systemd` to manage the lifecycle of the docker container in the system. 81 | 82 | ### Upload a file with the client 83 | 84 | Now that the server is up and running, you can upload files with this command: 85 | 86 | ``` 87 | gom build bin/client/client.go 88 | ./client file1 file2 file3 ... 89 | ``` 90 | 91 | it'll return the URL to share/delete the uploaded files. Example: 92 | ``` 93 | $ ./client --keep -ttl=4h README.md 94 | For file : README.md 95 | URL: http://localhost:9000/upd/README.md 96 | Delete URL: http://localhost:9000/upd/README.md/ytGsotfcIUuZZ6eL 97 | Available until: 2015-01-24 23:01:18.452801595 +0100 CET 98 | ``` 99 | 100 | Available flags for the `client` executable: 101 | 102 | ``` 103 | -search-tags="": Search by tags. If many, must be separated by a comma, an 'or' operator is used. Ex: "may,screenshot". 104 | -ca="none": For HTTPS support: none / filename of an accepted CA / unsafe (doesn't check the CA) 105 | -key="": A shared secret key to identify the client. 106 | -tags="": Tag the files. Ex: -tags="screenshot,may" 107 | -ttl="": TTL after which the file expires, ex: 30m. Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h" 108 | -url="http://localhost:9000/upd": The upd server to contact. 109 | ``` 110 | -------------------------------------------------------------------------------- /src/server/serving_handler.go: -------------------------------------------------------------------------------- 1 | // Route to server the files. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package server 5 | 6 | import ( 7 | "bytes" 8 | "image" 9 | "image/jpeg" 10 | "image/png" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | "strconv" 15 | "time" 16 | 17 | "github.com/gorilla/mux" 18 | "github.com/nfnt/resize" 19 | ) 20 | 21 | const ( 22 | HEADER_ORIGINAL_FILENAME = "X-Upd-Orig-Filename" 23 | ) 24 | 25 | type ServingHandler struct { 26 | Server *Server // pointer to the started server 27 | } 28 | 29 | func (s *ServingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 30 | // Parse the route parameters 31 | vars := mux.Vars(r) 32 | 33 | id := vars["file"] 34 | 35 | // Some check on the file id 36 | if len(id) == 0 { 37 | w.WriteHeader(404) 38 | return 39 | } 40 | 41 | // Look for the file in BoltDB 42 | entry, err := s.Server.GetEntry(id) 43 | if err != nil { 44 | log.Println("[err] Error while retrieving an entry:", err.Error()) 45 | w.WriteHeader(500) 46 | return 47 | } 48 | 49 | // Existing file ? 50 | if entry == nil || entry.Filename == "" { 51 | w.WriteHeader(404) 52 | return 53 | } 54 | 55 | // Existing, serve the file ! 56 | 57 | // but first, check that it hasn't expired 58 | if entry.TTL != "" { 59 | duration, _ := time.ParseDuration(entry.TTL) 60 | now := time.Now() 61 | fileEndlife := entry.CreationTime.Add(duration) 62 | if fileEndlife.Before(now) { 63 | // No longer alive! 64 | err := s.Server.Expire(*entry) 65 | if err != nil { 66 | log.Println("[warn] While deleting file:", entry.Filename) 67 | log.Println(err) 68 | } else { 69 | log.Println("[info] Deleted due to TTL:", entry.Filename) 70 | } 71 | 72 | w.WriteHeader(404) 73 | return 74 | } 75 | } 76 | 77 | // read it 78 | data, err := s.Server.ReadFile(entry.Filename) 79 | 80 | if err != nil { 81 | log.Println("[err] Can't read the file from the storage.") 82 | log.Println(err) 83 | w.WriteHeader(500) 84 | return 85 | } 86 | 87 | // detect the content-type 88 | contentType := http.DetectContentType(data) 89 | 90 | // we'll see whether or not we want to generate a thumbnail 91 | r.ParseForm() 92 | width := r.Form.Get("w") 93 | height := r.Form.Get("h") 94 | if len(width) != 0 && len(height) != 0 { 95 | 96 | iwidth, err := strconv.Atoi(width) 97 | if err != nil { 98 | w.WriteHeader(400) 99 | return 100 | } 101 | iheight, err := strconv.Atoi(height) 102 | if err != nil { 103 | w.WriteHeader(400) 104 | return 105 | } 106 | 107 | if iheight < 0 || iwidth < 0 { 108 | w.WriteHeader(400) 109 | return 110 | } 111 | 112 | // don't permit too large resize 113 | if iheight > 500 || iwidth > 500 { 114 | w.WriteHeader(400) 115 | return 116 | } 117 | 118 | data = s.Resize(id, contentType, data, uint(iwidth), uint(iheight)) 119 | } 120 | 121 | w.Header().Set("Content-Type", contentType) 122 | w.Header().Set(HEADER_ORIGINAL_FILENAME, entry.Original) 123 | w.Header().Set("Content-Disposition", "inline; filename*=UTF-8''"+url.QueryEscape(entry.Original)) 124 | w.Write(data) 125 | } 126 | 127 | func (s *ServingHandler) Resize(id string, contentType string, data []byte, width uint, height uint) []byte { 128 | if len(data) == 0 { 129 | return data 130 | } 131 | if contentType != "image/png" && contentType != "image/jpeg" { 132 | return data 133 | } 134 | 135 | var err error 136 | var img image.Image 137 | var buffer *bytes.Buffer 138 | 139 | // create the Image instance 140 | if contentType == "image/png" { 141 | img, err = png.Decode(bytes.NewReader(data)) 142 | if err != nil { 143 | log.Println("[err] Can't resize png image with id:", id) 144 | return data 145 | } 146 | } else if contentType == "image/jpeg" { 147 | img, err = jpeg.Decode(bytes.NewReader(data)) 148 | if err != nil { 149 | log.Println("[err] Can't resize jpg image with id:", id) 150 | return data 151 | } 152 | } 153 | 154 | // resize the image 155 | img = resize.Resize(width, height, img, resize.Lanczos3) 156 | 157 | // write the data 158 | buffer = bytes.NewBuffer(nil) 159 | if contentType == "image/png" { 160 | err = png.Encode(buffer, img) 161 | if err != nil { 162 | log.Println("[err] Can't encode png image with id:", id) 163 | return data 164 | } 165 | } else if contentType == "image/jpeg" { 166 | err = jpeg.Encode(buffer, img, nil) 167 | if err != nil { 168 | log.Println("[err] Can't encode jpg image with id:", id) 169 | return data 170 | } 171 | } 172 | 173 | return buffer.Bytes() 174 | } 175 | -------------------------------------------------------------------------------- /src/server/send_handler.go: -------------------------------------------------------------------------------- 1 | // Route receiving the data when a file is uploaded. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | package server 4 | 5 | import ( 6 | "encoding/json" 7 | "io/ioutil" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "github.com/boltdb/bolt" 16 | ) 17 | 18 | type SendHandler struct { 19 | Server *Server // pointer to the started server 20 | } 21 | 22 | const ( 23 | SECRET_KEY_HEADER = "X-upd-key" 24 | ) 25 | 26 | // Json returned to the client 27 | type SendResponse struct { 28 | Name string `json:"name"` 29 | DeleteKey string `json:"delete_key"` 30 | ExpirationTime time.Time `json:"expiration_time"` 31 | } 32 | 33 | const ( 34 | MAX_MEMORY = 1024 * 1024 35 | DICTIONARY = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 36 | ) 37 | 38 | func (s *SendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 39 | // checks the secret key 40 | if !IsAuthValid(s.Server, r) { 41 | w.WriteHeader(403) 42 | return 43 | } 44 | 45 | // parse the form 46 | reader, _, err := r.FormFile("data") 47 | 48 | if err != nil { 49 | w.WriteHeader(500) 50 | log.Println("[err] Error while receiving data (FormFile).") 51 | log.Println(err) 52 | return 53 | } 54 | 55 | // read the data 56 | data, err := ioutil.ReadAll(reader) 57 | if err != nil { 58 | w.WriteHeader(500) 59 | log.Println("[err] Error while receiving data (ReadAll).") 60 | log.Println(err) 61 | return 62 | } 63 | 64 | // write the file in the directory 65 | var name string 66 | var original string 67 | 68 | // name 69 | if len(r.Form["name"]) == 0 { 70 | w.WriteHeader(400) 71 | return 72 | } else { 73 | original = filepath.Base(r.Form["name"][0]) 74 | } 75 | 76 | for { 77 | name = s.randomString(8) 78 | // test existence 79 | entry, err := s.Server.GetEntry(name) 80 | if err != nil { 81 | log.Println("[err] While reading the database:", err.Error()) 82 | w.WriteHeader(500) 83 | return 84 | } 85 | if entry == nil || entry.Filename == "" { 86 | break 87 | } 88 | } 89 | 90 | var expirationTime time.Time 91 | now := time.Now() 92 | 93 | // reads the TTL 94 | var ttl string 95 | if len(r.Form["ttl"]) > 0 { 96 | ttl = r.Form["ttl"][0] 97 | // check that the value is a correct duration 98 | _, err := time.ParseDuration(ttl) 99 | if err != nil { 100 | println(err.Error()) 101 | w.WriteHeader(400) 102 | return 103 | } 104 | 105 | // compute the expiration time 106 | expirationTime = s.Server.computeEndOfLife(ttl, now) 107 | } 108 | 109 | // reads the tags 110 | tags := make([]string, 0) 111 | if len(r.Form["tags"]) > 0 { 112 | tags = strings.Split(r.Form["tags"][0], ",") 113 | } 114 | 115 | // writes the data on the storage 116 | if err := s.Server.WriteFile(name, data); err != nil { 117 | log.Println("[err] unable to write file to storage", err) 118 | w.WriteHeader(500) 119 | return 120 | } 121 | 122 | // add to metadata 123 | deleteKey := s.randomString(16) 124 | if err := s.addMetadata(name, original, tags, expirationTime, ttl, deleteKey, now); err != nil { 125 | log.Println("[err] unable to add metadata", err) 126 | w.WriteHeader(500) 127 | return 128 | } 129 | 130 | // encode the response json 131 | response := SendResponse{ 132 | Name: name, 133 | DeleteKey: deleteKey, 134 | ExpirationTime: expirationTime, 135 | } 136 | 137 | resp, _ := json.Marshal(response) 138 | 139 | w.Header().Set("Content-Type", "application/json") 140 | w.Write(resp) 141 | } 142 | 143 | // randomString generates a random valid URL string of the given size 144 | func (s *SendHandler) randomString(size int) string { 145 | result := "" 146 | 147 | for i := 0; i < size; i++ { 148 | result += string(DICTIONARY[rand.Int31n(int32(len(DICTIONARY)))]) 149 | } 150 | 151 | return result 152 | } 153 | 154 | // addMetadata adds the given entry to the Server metadata information. 155 | func (s *SendHandler) addMetadata(name string, original string, tags []string, expirationTime time.Time, ttl string, key string, now time.Time) error { 156 | metadata := Metadata{ 157 | Filename: name, 158 | Original: original, 159 | Tags: tags, 160 | TTL: ttl, 161 | ExpirationTime: expirationTime, 162 | DeleteKey: key, 163 | CreationTime: now, 164 | } 165 | 166 | // marshal the object 167 | data, err := json.Marshal(metadata) 168 | if err != nil { 169 | log.Println("[err] Can't marshal an object to store it", err) 170 | return err 171 | } 172 | 173 | // store into BoltDB 174 | err = s.Server.Database.Update(func(tx *bolt.Tx) error { 175 | bucket := tx.Bucket([]byte("Metadata")) 176 | return bucket.Put([]byte(name), data) 177 | }) 178 | 179 | if err != nil { 180 | log.Println("[err] Can't store") 181 | log.Println(string(data)) 182 | log.Printf("[err] Reason: %s\n", err.Error()) 183 | return err 184 | } 185 | 186 | // store the LastUploaded infos 187 | lastUploaded, err := s.Server.GetLastUploaded() 188 | if err != nil { 189 | log.Println("[err] Can't read the last uploaded in send handler:", err.Error()) 190 | return err 191 | } 192 | 193 | lastUploaded = append([]string{name}, lastUploaded[:]...) 194 | if len(lastUploaded) > MAX_LAST_UPLOADED { 195 | lastUploaded = lastUploaded[:len(lastUploaded)-1] 196 | } 197 | 198 | s.Server.SetLastUploaded(lastUploaded) 199 | 200 | if err != nil { 201 | log.Printf("[err] Can't store the LastUploaded infos for %s, reason: %s\n", name, err.Error()) 202 | return err 203 | } 204 | 205 | return nil 206 | } 207 | -------------------------------------------------------------------------------- /src/server/file.go: -------------------------------------------------------------------------------- 1 | // Methods dealing with files writing/reading 2 | // from either the FS or a distant service (s3, ...) 3 | // Copyright © 2015 - Rémy MATHIEU 4 | package server 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "time" 14 | 15 | "github.com/aws/aws-sdk-go/aws" 16 | "github.com/aws/aws-sdk-go/aws/credentials" 17 | "github.com/aws/aws-sdk-go/aws/session" 18 | "github.com/aws/aws-sdk-go/service/s3" 19 | ) 20 | 21 | func (s *Server) createS3Config(creds *credentials.Credentials, region string) *aws.Config { 22 | return &aws.Config{ 23 | Credentials: creds, 24 | Region: ®ion, 25 | } 26 | } 27 | 28 | // writeFile deals with writing the file, using the flags 29 | // to know where and the filename / data to store it. 30 | func (s *Server) WriteFile(filename string, data []byte) error { 31 | if s.Config.Storage == FS_STORAGE { 32 | fi, err := os.Stat(s.Config.FSConfig.OutputDirectory) 33 | if err != nil && !os.IsNotExist(err) { 34 | log.Println("[err] Error while stat'ing the output directory ", s.Config.FSConfig.OutputDirectory) 35 | return err 36 | } 37 | 38 | if os.IsNotExist(err) { 39 | err = os.MkdirAll(s.Config.FSConfig.OutputDirectory, 0755) 40 | if err != nil { 41 | log.Println("[err] Error while creating the output directory", s.Config.FSConfig.OutputDirectory) 42 | return err 43 | } 44 | } 45 | 46 | if fi != nil && !fi.IsDir() { 47 | return fmt.Errorf("%s is not a directory", s.Config.FSConfig.OutputDirectory) 48 | } 49 | 50 | file, err := os.Create(s.Config.FSConfig.OutputDirectory + "/" + filename) 51 | if err != nil { 52 | log.Println("[err] Can't create the file to write: ", filename) 53 | return err 54 | } 55 | 56 | _, err = file.Write(data) 57 | if err != nil { 58 | log.Println("[err] Can't write the file to write: ", filename) 59 | return err 60 | } 61 | 62 | err = file.Close() 63 | if err != nil { 64 | log.Println("[err] Can't close the file to write: ", filename) 65 | return err 66 | } 67 | return nil 68 | } else if s.Config.Storage == S3_STORAGE { 69 | // S3 connection 70 | creds := credentials.NewStaticCredentials(s.Config.S3Config.AccessKey, s.Config.S3Config.AccessSecret, "") 71 | sess := session.New(&aws.Config{ 72 | Credentials: creds, 73 | Region: aws.String(s.Config.S3Config.Region), 74 | }) 75 | client := s3.New(sess) 76 | body := bytes.NewReader(data) 77 | 78 | contentLength := int64(len(data)) 79 | 80 | // Creates the S3 put request 81 | por := &s3.PutObjectInput{ 82 | Body: body, 83 | Key: aws.String(filename), 84 | ContentLength: &contentLength, 85 | Bucket: aws.String(s.Config.S3Config.Bucket), 86 | } 87 | 88 | // Sends the S3 put request 89 | _, err := client.PutObject(por) 90 | if err != nil { 91 | return err 92 | } 93 | return nil 94 | } 95 | 96 | return fmt.Errorf("[err] Unsupported storage: %s", s.Config.Storage) 97 | } 98 | 99 | // readFile is the method to read the file from wherever it 100 | // is stored. The serverFlags are used to know where to read, 101 | // the filename is used to know what to read. 102 | func (s *Server) ReadFile(filename string) ([]byte, error) { 103 | if s.Config.Storage == FS_STORAGE { 104 | file, err := os.Open(s.Config.FSConfig.OutputDirectory + "/" + filename) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | data, err := ioutil.ReadAll(file) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | return data, nil 115 | } else if s.Config.Storage == S3_STORAGE { 116 | // S3 connection 117 | creds := credentials.NewStaticCredentials(s.Config.S3Config.AccessKey, s.Config.S3Config.AccessSecret, "") 118 | sess := session.New(&aws.Config{ 119 | Credentials: creds, 120 | Region: aws.String(s.Config.S3Config.Region), 121 | }) 122 | client := s3.New(sess) 123 | 124 | // The get request 125 | gor := &s3.GetObjectInput{ 126 | Key: aws.String(filename), 127 | Bucket: aws.String(s.Config.S3Config.Bucket), 128 | } 129 | 130 | // Sends the request 131 | resp, err := client.GetObject(gor) 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | // Reads the result 137 | data, err := ioutil.ReadAll(resp.Body) 138 | if err != nil { 139 | log.Println("[err] Can't read the body of a GetObjectOutput from AWS") 140 | log.Println(err) 141 | return nil, err 142 | } 143 | 144 | return data, nil 145 | } 146 | 147 | return nil, fmt.Errorf("[err] Unsupported storage: %s", s.Config.Storage) 148 | } 149 | 150 | // Expire expires a file : delete it from the metadata 151 | // and from the FS. 152 | func (s *Server) Expire(m Metadata) error { 153 | filename := m.Filename 154 | 155 | // delete from the datbase 156 | s.deleteMetadata(filename) 157 | 158 | if s.Config.Storage == FS_STORAGE { 159 | return os.Remove(filepath.Join(s.Config.FSConfig.OutputDirectory, filename)) 160 | } else if s.Config.Storage == S3_STORAGE { 161 | // S3 connection 162 | creds := credentials.NewStaticCredentials(s.Config.S3Config.AccessKey, s.Config.S3Config.AccessSecret, "") 163 | sess := session.New(&aws.Config{ 164 | Credentials: creds, 165 | Region: aws.String(s.Config.S3Config.Region), 166 | }) 167 | client := s3.New(sess) 168 | 169 | // The get request 170 | dor := &s3.DeleteObjectInput{ 171 | Key: aws.String(filename), 172 | Bucket: aws.String(s.Config.S3Config.Bucket), 173 | } 174 | 175 | _, err := client.DeleteObject(dor) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | return nil 181 | } 182 | 183 | return fmt.Errorf("[err] Unsupported storage: %s", s.Config.Storage) 184 | } 185 | 186 | // computeEndOfLife return as a string the end of life of the new file. 187 | func (s *Server) computeEndOfLife(ttl string, now time.Time) time.Time { 188 | if len(ttl) == 0 { 189 | return time.Time{} 190 | } 191 | duration, _ := time.ParseDuration(ttl) // no error possible 'cause already checked in the controller 192 | t := now.Add(duration) 193 | return t 194 | } 195 | -------------------------------------------------------------------------------- /src/server/server.go: -------------------------------------------------------------------------------- 1 | // Instance of the server. 2 | // Copyright © 2015 - Rémy MATHIEU 3 | 4 | package server 5 | 6 | import ( 7 | "encoding/json" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "os" 12 | "time" 13 | 14 | "github.com/boltdb/bolt" 15 | "github.com/gorilla/mux" 16 | ) 17 | 18 | const ( 19 | LAST_UPLOADED_KEY = "LastUploaded" 20 | ) 21 | 22 | type Server struct { 23 | Config Config // Configuration 24 | Database *bolt.DB // opened bolt db 25 | Storage string // Storage used with this metadata file. 26 | } 27 | 28 | func NewServer(config Config) *Server { 29 | // init the random 30 | rand.Seed(time.Now().Unix()) 31 | 32 | return &Server{ 33 | Config: config, 34 | Storage: config.Storage, 35 | } 36 | } 37 | 38 | // Starts the listening daemon. 39 | func (s *Server) Start() { 40 | router := s.prepareRouter() 41 | 42 | // Setup the router on the net/http stack 43 | http.Handle("/", router) 44 | 45 | // Open the database 46 | s.openBoltDatabase() 47 | 48 | go s.StartCleanJob() 49 | 50 | // Listen 51 | if len(s.Config.CertificateFile) != 0 && len(s.Config.CertificateKey) != 0 { 52 | log.Println("[info] Start secure listening on", s.Config.Addr) 53 | err := http.ListenAndServeTLS(s.Config.Addr, s.Config.CertificateFile, s.Config.CertificateKey, nil) 54 | log.Println("[err]", err.Error()) 55 | } else { 56 | log.Println("[info] Start listening on", s.Config.Addr) 57 | err := http.ListenAndServe(s.Config.Addr, nil) 58 | log.Println("[err]", err.Error()) 59 | } 60 | } 61 | 62 | // Starts the Clean Job 63 | func (s *Server) StartCleanJob() { 64 | timer := time.NewTicker(60 * time.Second) 65 | for range timer.C { 66 | job := CleanJob{s} 67 | job.Run() 68 | } 69 | } 70 | 71 | // writeBoltMetadata stores the metadata in a BoltDB file. 72 | func (s *Server) openBoltDatabase() { 73 | db, err := bolt.Open(s.Config.RuntimeDir+"/metadata.db", 0600, nil) 74 | if err != nil { 75 | log.Println("[err] Can't open the metadata.db file in :", s.Config.RuntimeDir) 76 | log.Println(err) 77 | os.Exit(1) 78 | } 79 | 80 | log.Printf("[info] %s opened.", s.Config.RuntimeDir+"/metadata.db") 81 | 82 | s.Database = db 83 | 84 | // creates the bucket if needed 85 | db.Update(func(tx *bolt.Tx) error { 86 | _, err := tx.CreateBucketIfNotExists([]byte("Metadata")) 87 | if err != nil { 88 | log.Println("Can't create the bucket 'Metadata'") 89 | log.Println(err) 90 | } 91 | _, err = tx.CreateBucketIfNotExists([]byte("Runtime")) 92 | if err != nil { 93 | log.Println("Can't create the bucket 'Runtime'") 94 | log.Println(err) 95 | } 96 | _, err = tx.CreateBucketIfNotExists([]byte("Config")) 97 | if err != nil { 98 | log.Println("Can't create the bucket 'LastUploaded'") 99 | log.Println(err) 100 | } 101 | return err 102 | }) 103 | 104 | // test that the storage is still the same 105 | var mustSave bool 106 | s.Database.View(func(tx *bolt.Tx) error { 107 | bucket := tx.Bucket([]byte("Config")) 108 | v := bucket.Get([]byte("storage")) 109 | if v == nil { 110 | mustSave = true 111 | return nil 112 | } 113 | 114 | if string(v) != s.Config.Storage { 115 | log.Printf("The database use the storage %s, can't start with the storage %s\n", string(v), s.Config.Storage) 116 | os.Exit(1) 117 | } 118 | return nil 119 | }) 120 | 121 | // save the storage 122 | if mustSave { 123 | s.Database.Update(func(tx *bolt.Tx) error { 124 | bucket := tx.Bucket([]byte("Config")) 125 | bucket.Put([]byte("storage"), []byte(s.Config.Storage)) 126 | return nil 127 | }) 128 | } 129 | } 130 | 131 | func (s *Server) deleteMetadata(name string) error { 132 | err := s.Database.Update(func(tx *bolt.Tx) error { 133 | bucket := tx.Bucket([]byte("Metadata")) 134 | return bucket.Delete([]byte(name)) 135 | }) 136 | if err != nil { 137 | log.Println("Can't delete some metadata from the database:") 138 | log.Println(err) 139 | } 140 | 141 | return err 142 | } 143 | 144 | // getEntry looks in the Bolt DB whether this entry exists and returns it 145 | // if found, otherwise, nil is returned. 146 | func (s *Server) GetEntry(id string) (*Metadata, error) { 147 | var metadata *Metadata 148 | err := s.Database.View(func(tx *bolt.Tx) error { 149 | bucket := tx.Bucket([]byte("Metadata")) 150 | v := bucket.Get([]byte(id)) 151 | if v == nil { 152 | return nil 153 | } 154 | 155 | metadata = new(Metadata) 156 | 157 | // unmarshal the bytes 158 | err := json.Unmarshal(v, &metadata) 159 | if err != nil { 160 | return err 161 | } 162 | 163 | return nil 164 | }) 165 | 166 | return metadata, err 167 | } 168 | 169 | // GetLastUploaded reads into BoltDB the array 170 | // of last uploaded entries. 171 | func (s *Server) GetLastUploaded() ([]string, error) { 172 | values := make([]string, 0) 173 | err := s.Database.View(func(tx *bolt.Tx) error { 174 | bucket := tx.Bucket([]byte("Runtime")) 175 | v := bucket.Get([]byte(LAST_UPLOADED_KEY)) 176 | 177 | if v == nil { 178 | return nil 179 | } 180 | 181 | err := json.Unmarshal(v, &values) 182 | return err 183 | }) 184 | 185 | return values, err 186 | } 187 | 188 | func (s *Server) SetLastUploaded(lastUploaded []string) error { 189 | return s.Database.Update(func(tx *bolt.Tx) error { 190 | d, err := json.Marshal(lastUploaded) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | bucket := tx.Bucket([]byte("Runtime")) 196 | return bucket.Put([]byte(LAST_UPLOADED_KEY), d) 197 | }) 198 | } 199 | 200 | // Prepares the route 201 | func (s *Server) prepareRouter() http.Handler { 202 | r := mux.NewRouter() 203 | 204 | println(s.Config.Route) 205 | sendHandler := &SendHandler{s} 206 | r.Handle(s.Config.Route+"/1.0/send", sendHandler) 207 | 208 | lastUploadeHandler := &LastUploadedHandler{s} 209 | r.Handle(s.Config.Route+"/1.0/list", lastUploadeHandler) 210 | 211 | searchTagsHandler := &SearchTagsHandler{s} 212 | r.Handle(s.Config.Route+"/1.0/search_tags", searchTagsHandler) 213 | 214 | authCheckHandler := &AuthCheckHandler{s} 215 | r.Handle(s.Config.Route+"/1.0/auth_check", authCheckHandler) 216 | 217 | deleteHandler := &DeleteHandler{s} 218 | r.Handle(s.Config.Route+"/{file}/{key}", deleteHandler) 219 | 220 | sh := &ServingHandler{s} 221 | r.Handle(s.Config.Route+"/{file}", sh) // Serving route. 222 | 223 | // Wrap it into a CORS handler, so we can use AJAX with UPD 224 | return &CorsHandler{r} 225 | } 226 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2015 Rémy Mathieu 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | --------------------------------------------------------------------------------