diff --git a/Metadata.go b/Metadata.go index 193e3c7..b635e28 100644 --- a/Metadata.go +++ b/Metadata.go @@ -2,13 +2,21 @@ package contented import ( "encoding/json" + "errors" "os" "time" "github.com/boltdb/bolt" + "github.com/speps/go-hashids" +) + +const ( + hashIdSalt = "contented" + hashIdMinLength = 2 ) type Metadata struct { + FileHash string FileSize int64 UploadTime time.Time UploadIP string @@ -33,13 +41,35 @@ func (this *Server) Metadata(fileID string) (*Metadata, error) { return &m, nil } -func (this *Server) SetMetadata(fileID string, m Metadata) error { +func idToString(v uint64) string { + hd := hashids.NewData() + hd.Salt = hashIdSalt + hd.MinLength = hashIdMinLength + h, _ := hashids.NewWithData(hd) + s, _ := h.EncodeInt64([]int64{int64(v)}) + return s +} + +func (this *Server) AddMetadata(m Metadata) (string, error) { jb, err := json.Marshal(m) if err != nil { - return err + return "", err } - return this.db.Update(func(tx *bolt.Tx) error { - return tx.Bucket(this.metadataBucket).Put([]byte(fileID), jb) + var shortRef string = "" + + err = this.db.Update(func(tx *bolt.Tx) error { + b := tx.Bucket(this.metadataBucket) + seq, _ := b.NextSequence() // cannot fail + shortRef = idToString(seq) + return tx.Bucket(this.metadataBucket).Put([]byte(shortRef), jb) }) + if err != nil { + return "", err + } + if shortRef == "" { + return "", errors.New("Invalid URL generated") + } + + return shortRef, nil } diff --git a/Server.go b/Server.go index 88ff2ab..8f65c68 100644 --- a/Server.go +++ b/Server.go @@ -5,7 +5,6 @@ import ( "log" "net/http" "os" - "path/filepath" "strings" "github.com/boltdb/bolt" @@ -56,33 +55,6 @@ func NewServer(opts *ServerOptions) (*Server, error) { return s, nil } -func (this *Server) handleView(w http.ResponseWriter, r *http.Request, fileID string) error { - // Load file - f, err := os.Open(filepath.Join(this.opts.DataDirectory, fileID)) - if err != nil { - return err - } - - defer f.Close() - - // Load metadata - m, err := this.Metadata(fileID) - if err != nil { - return err - } - - // ServeContent only uses the filename to get the mime type, which we can - // set accurately (including blacklist) - w.Header().Set(`Content-Type`, m.MimeType) - if m.MimeType == `application/octet-stream` { - w.Header().Set(`Content-Disposition`, `attachment; filename="`+m.Filename+`"`) - } - - http.ServeContent(w, r, "", m.UploadTime, f) - - return nil -} - func (this *Server) handleInformation(w http.ResponseWriter, fileID string) { m, err := this.Metadata(fileID) if err != nil { diff --git a/_dist/README.txt b/_dist/README.txt index 1935ff3..f5e6d70 100644 --- a/_dist/README.txt +++ b/_dist/README.txt @@ -15,6 +15,7 @@ You can use contented as a standalone upload server, or you can use the SDK to e - Hash verification (SHA512/256) - Detect duplicate upload content and reuse storage - Options to limit the upload filesize and the upload bandwidth +- Short URLs (using [url=http://hashids.org]Hashids[/url] algorithm) =USAGE (SERVER)= @@ -50,6 +51,5 @@ contented.init("#target", function(/* String[] */ items) {}); - View upload history / view all uploads - Encrypted at rest (anti- provider snooping) -- Shorter URLs - Deploy! - Display 'my uploads' (id + metadata history kept in localstorage) diff --git a/download.go b/download.go new file mode 100644 index 0000000..fc09b43 --- /dev/null +++ b/download.go @@ -0,0 +1,35 @@ +package contented + +import ( + "net/http" + "os" + "path/filepath" +) + +func (this *Server) handleView(w http.ResponseWriter, r *http.Request, fileID string) error { + + // Load metadata + m, err := this.Metadata(fileID) + if err != nil { + return err + } + + // Load file + f, err := os.Open(filepath.Join(this.opts.DataDirectory, m.FileHash)) + if err != nil { + return err + } + + defer f.Close() + + // ServeContent only uses the filename to get the mime type, which we can + // set accurately (including blacklist) + w.Header().Set(`Content-Type`, m.MimeType) + if m.MimeType == `application/octet-stream` { + w.Header().Set(`Content-Disposition`, `attachment; filename="`+m.Filename+`"`) + } + + http.ServeContent(w, r, "", m.UploadTime, f) + + return nil +} diff --git a/upload.go b/upload.go index a85df15..d3a40a7 100644 --- a/upload.go +++ b/upload.go @@ -88,20 +88,24 @@ func (this *Server) handleUploadFile(src multipart.File, hdr *multipart.FileHead } // Save file to disk - fileID := hex.EncodeToString(hasher.Sum(nil)) - dest, err := os.OpenFile(filepath.Join(this.opts.DataDirectory, fileID), os.O_CREATE|os.O_WRONLY, 0600) - if err != nil { - if os.IsExist(err) { - return fileID, nil // hash matches existing upload - } - + fileHash := hex.EncodeToString(hasher.Sum(nil)) + dest, err := os.OpenFile(filepath.Join(this.opts.DataDirectory, fileHash), os.O_CREATE|os.O_WRONLY, 0600) + shouldSave := true + if err != nil && os.IsExist(err) { + // hash matches existing upload + // That's fine - but still persist the metadata separately + shouldSave = false + } else if err != nil { return "", err } - defer dest.Close() - _, err = io.CopyN(dest, src, int64(srcLen)) - if err != nil { - return "", err + if shouldSave { + defer dest.Close() + + _, err = io.CopyN(dest, src, int64(srcLen)) + if err != nil { + return "", err + } } // Determine mime type @@ -122,17 +126,19 @@ func (this *Server) handleUploadFile(src multipart.File, hdr *multipart.FileHead // Persist metadata to DB m := Metadata{ + FileHash: fileHash, Filename: hdr.Filename, UploadTime: time.Now(), UploadIP: UploadIP, FileSize: srcLen, MimeType: ctype, } - err = this.SetMetadata(fileID, m) + + fileRef, err := this.AddMetadata(m) if err != nil { return "", err } // Done - return fileID, nil + return fileRef, nil }