First version

This commit is contained in:
Ice 2014-04-09 17:00:48 +07:00
parent 656f7ddb5c
commit 717a0b72de
4 changed files with 84 additions and 16 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@ -61,9 +61,9 @@ func main() {
// Methods clients are allowed to use // Methods clients are allowed to use
w.Header().Add("Access-Control-Allow-Methods", "HEAD,GET,PUT,POST,PATCH,DELETE") w.Header().Add("Access-Control-Allow-Methods", "HEAD,GET,PUT,POST,PATCH,DELETE")
// Headers clients are allowed to send // Headers clients are allowed to send
w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Content-Disposition, Final-Length, Offset") w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Content-Disposition, Final-Length, Offset, File-Type")
// Headers clients are allowed to receive // Headers clients are allowed to receive
w.Header().Add("Access-Control-Expose-Headers", "Location, Range, Content-Disposition, Offset") w.Header().Add("Access-Control-Expose-Headers", "Location, md5Value, Range, Content-Disposition, Offset")
if r.Method == "OPTIONS" { if r.Method == "OPTIONS" {
return return

View File

@ -19,7 +19,7 @@ const defaultFilePerm = 0666
type dataStore struct { type dataStore struct {
dir string dir string
maxSize int64 maxSize int64
fileType string
// infoLocksLock locks the infosLocks map // infoLocksLock locks the infosLocks map
infoLocksLock *sync.Mutex infoLocksLock *sync.Mutex
// infoLocks locks the .info files // infoLocks locks the .info files
@ -50,8 +50,8 @@ func (s *dataStore) infoLock(id string) *sync.RWMutex {
return lock return lock
} }
func (s *dataStore) CreateFile(id string, finalLength int64, meta map[string]string) error { func (s *dataStore) CreateFile(id string, fileType string, finalLength int64, meta map[string]string) error {
file, err := os.OpenFile(s.filePath(id), os.O_CREATE|os.O_WRONLY, defaultFilePerm) file, err := os.OpenFile(s.filePath(id, fileType), os.O_CREATE|os.O_WRONLY, defaultFilePerm)
if err != nil { if err != nil {
return err return err
} }
@ -63,8 +63,8 @@ func (s *dataStore) CreateFile(id string, finalLength int64, meta map[string]str
return s.writeInfo(id, FileInfo{FinalLength: finalLength, Meta: meta}) return s.writeInfo(id, FileInfo{FinalLength: finalLength, Meta: meta})
} }
func (s *dataStore) WriteFileChunk(id string, offset int64, src io.Reader) error { func (s *dataStore) WriteFileChunk(id string, fileType string, offset int64, src io.Reader) error {
file, err := os.OpenFile(s.filePath(id), os.O_WRONLY, defaultFilePerm) file, err := os.OpenFile(s.filePath(id, fileType), os.O_WRONLY, defaultFilePerm)
if err != nil { if err != nil {
return err return err
} }
@ -85,8 +85,8 @@ func (s *dataStore) WriteFileChunk(id string, offset int64, src io.Reader) error
return err return err
} }
func (s *dataStore) ReadFile(id string) (io.ReadCloser, error) { func (s *dataStore) ReadFile(id string, fileType string) (io.ReadCloser, error) {
return os.Open(s.filePath(id)) return os.Open(s.filePath(id, fileType))
} }
func (s *dataStore) GetInfo(id string) (FileInfo, error) { func (s *dataStore) GetInfo(id string) (FileInfo, error) {
@ -138,8 +138,8 @@ func (s *dataStore) setOffset(id string, offset int64) error {
return s.writeInfo(id, info) return s.writeInfo(id, info)
} }
func (s *dataStore) filePath(id string) string { func (s *dataStore) filePath(id string, fileType string) string {
return path.Join(s.dir, id) + ".bin" return path.Join(s.dir, id) + "." + fileType
} }
func (s *dataStore) infoPath(id string) string { func (s *dataStore) infoPath(id string) string {

View File

@ -10,6 +10,8 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"crypto/md5"
"encoding/hex"
) )
var fileUrlMatcher = regexp.MustCompile("^/([a-z0-9]{32})$") var fileUrlMatcher = regexp.MustCompile("^/([a-z0-9]{32})$")
@ -117,6 +119,12 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) { func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
id := uid() id := uid()
fileType, err := getStringHeader(r, "File-Type")
if err != nil {
h.err(err, w, http.StatusBadRequest)
return
}
finalLength, err := getPositiveIntHeader(r, "Final-Length") finalLength, err := getPositiveIntHeader(r, "Final-Length")
if err != nil { if err != nil {
h.err(err, w, http.StatusBadRequest) h.err(err, w, http.StatusBadRequest)
@ -126,7 +134,7 @@ func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
// @TODO: Define meta data extension and implement it here // @TODO: Define meta data extension and implement it here
// @TODO: Make max finalLength configurable, reply with error if exceeded. // @TODO: Make max finalLength configurable, reply with error if exceeded.
// This should go into the protocol as well. // This should go into the protocol as well.
if err := h.store.CreateFile(id, finalLength, nil); err != nil { if err := h.store.CreateFile(id, fileType, finalLength, nil); err != nil {
h.err(err, w, http.StatusInternalServerError) h.err(err, w, http.StatusInternalServerError)
return return
} }
@ -137,6 +145,13 @@ func (h *Handler) createFile(w http.ResponseWriter, r *http.Request) {
func (h *Handler) patchFile(w http.ResponseWriter, r *http.Request, id string) { func (h *Handler) patchFile(w http.ResponseWriter, r *http.Request, id string) {
offset, err := getPositiveIntHeader(r, "Offset") offset, err := getPositiveIntHeader(r, "Offset")
//contentLength, err := getPositiveIntHeader(r, "Content-Length")
if err != nil {
h.err(err, w, http.StatusBadRequest)
return
}
fileType, err := getStringHeader(r, "File-Type")
if err != nil { if err != nil {
h.err(err, w, http.StatusBadRequest) h.err(err, w, http.StatusBadRequest)
return return
@ -156,15 +171,24 @@ func (h *Handler) patchFile(w http.ResponseWriter, r *http.Request, id string) {
// @TODO Test offset < current offset // @TODO Test offset < current offset
err = h.store.WriteFileChunk(id, offset, r.Body) err = h.store.WriteFileChunk(id, fileType, offset, r.Body)
if err != nil { if err != nil {
// @TODO handle 404 properly (goes for all h.err calls) // @TODO handle 404 properly (goes for all h.err calls)
h.err(err, w, http.StatusInternalServerError) h.err(err, w, http.StatusInternalServerError)
return return
} }
w.Header().Set("md5Value", getMD5(h.config.Dir + "/" + id + "." + fileType))
} }
func (h *Handler) headFile(w http.ResponseWriter, r *http.Request, id string) { func (h *Handler) headFile(w http.ResponseWriter, r *http.Request, id string) {
fileType, err := getStringHeader(r, "File-Type")
if err != nil {
h.err(err, w, http.StatusBadRequest)
return
}
info, err := h.store.GetInfo(id) info, err := h.store.GetInfo(id)
if err != nil { if err != nil {
w.Header().Set("Content-Length", "0") w.Header().Set("Content-Length", "0")
@ -173,12 +197,23 @@ func (h *Handler) headFile(w http.ResponseWriter, r *http.Request, id string) {
} }
w.Header().Set("Offset", fmt.Sprintf("%d", info.Offset)) w.Header().Set("Offset", fmt.Sprintf("%d", info.Offset))
if info.Offset == info.FinalLength {
w.Header().Set("md5Value", getMD5(h.config.Dir + "/" + id + "." + fileType))
}
} }
// GET requests on files aren't part of the protocol yet, // GET requests on files aren't part of the protocol yet,
// but it is implemented here anyway for the demo. It still lacks the meta data // but it is implemented here anyway for the demo. It still lacks the meta data
// extension in order to send the proper content type header. // extension in order to send the proper content type header.
func (h *Handler) getFile(w http.ResponseWriter, r *http.Request, fileId string) { func (h *Handler) getFile(w http.ResponseWriter, r *http.Request, fileId string) {
fileType, err := getStringHeader(r, "File-Type")
if err != nil {
h.err(err, w, http.StatusBadRequest)
return
}
info, err := h.store.GetInfo(fileId) info, err := h.store.GetInfo(fileId)
if os.IsNotExist(err) { if os.IsNotExist(err) {
h.err(err, w, http.StatusNotFound) h.err(err, w, http.StatusNotFound)
@ -189,7 +224,7 @@ func (h *Handler) getFile(w http.ResponseWriter, r *http.Request, fileId string)
return return
} }
data, err := h.store.ReadFile(fileId) data, err := h.store.ReadFile(fileId,fileType)
if os.IsNotExist(err) { if os.IsNotExist(err) {
h.err(err, w, http.StatusNotFound) h.err(err, w, http.StatusNotFound)
return return
@ -213,6 +248,30 @@ func (h *Handler) getFile(w http.ResponseWriter, r *http.Request, fileId string)
} }
} }
func getMD5(filename string) (string) {
fi, err := os.Open(filename)
if err != nil { panic(err) }
defer func() {
if err := fi.Close(); err != nil {
panic(err)
}
}()
buf := make([]byte, 1024)
hash := md5.New()
for {
n, err := fi.Read(buf)
if err != nil && err != io.EOF { panic(err) }
if n == 0 { break }
if _, err := io.WriteString(hash, string(buf[:n])); err != nil {
panic(err)
}
}
return hex.EncodeToString(hash.Sum(nil))
}
func getPositiveIntHeader(r *http.Request, key string) (int64, error) { func getPositiveIntHeader(r *http.Request, key string) (int64, error) {
val := r.Header.Get(key) val := r.Header.Get(key)
if val == "" { if val == "" {
@ -228,6 +287,15 @@ func getPositiveIntHeader(r *http.Request, key string) (int64, error) {
return intVal, nil return intVal, nil
} }
func getStringHeader(r *http.Request, key string) (string, error) {
val := r.Header.Get(key)
if val == "" {
return "", errors.New(key + " header must not be empty")
}
return val, nil
}
// absUrl turn a relPath (e.g. "/foo") into an absolute url (e.g. // absUrl turn a relPath (e.g. "/foo") into an absolute url (e.g.
// "http://example.com/foo"). // "http://example.com/foo").
// //