First version
This commit is contained in:
parent
656f7ddb5c
commit
717a0b72de
|
@ -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
|
||||||
|
|
|
@ -17,9 +17,9 @@ const defaultFilePerm = 0666
|
||||||
|
|
||||||
// @TODO should not be exported for now, the API isn't stable / done well
|
// @TODO should not be exported for now, the API isn't stable / done well
|
||||||
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 {
|
||||||
|
|
|
@ -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").
|
||||||
//
|
//
|
||||||
|
|
Loading…
Reference in New Issue