[REV] Optimize the upload file process

This commit is contained in:
zhoujunhao 2018-09-28 13:09:25 +08:00
parent 24b1a2b5cc
commit 789609e67f
60 changed files with 74 additions and 2705 deletions

View File

@ -17,8 +17,8 @@ import (
"github.com/goodrain/rainbond/api/model"
"github.com/goodrain/rainbond/db"
httputil "github.com/goodrain/rainbond/util/http"
"github.com/goodrain/rainbond/api/controller/coquelicot.v1"
"github.com/jinzhu/gorm"
"github.com/goodrain/rainbond/api/controller/upload"
)
type AppStruct struct{}
@ -168,17 +168,8 @@ func (a *AppStruct) NewUpload(w http.ResponseWriter, r *http.Request) {
}
dirName := fmt.Sprintf("%s/import/%s", handler.GetAppHandler().GetStaticDir(), eventId)
he := coquelicot.NewStorage(dirName)
he.UploadHandler(w, r)
case "GET":
if eventId == "" {
httputil.ReturnError(r, w, 500, "Failed to parse eventId.")
return
}
dirName := fmt.Sprintf("%s/import/%s", handler.GetAppHandler().GetStaticDir(), eventId)
he := coquelicot.NewStorage(dirName)
he.ResumeHandler(w, r)
st := upload.NewStorage(dirName)
st.UploadHandler(w, r)
}
}
@ -276,7 +267,7 @@ func (a *AppStruct) ImportApp(w http.ResponseWriter, r *http.Request) {
res, err := db.GetManager().AppDao().GetByEventId(eventId)
if err != nil {
if err == gorm.ErrRecordNotFound{
if err == gorm.ErrRecordNotFound {
res.Status = "uploading"
httputil.ReturnSuccess(r, w, res)
return

View File

@ -1,4 +0,0 @@
*.swp
tags
bin/coquelicot/coquelicot
bin/coquelicot/dummy/root_storage/*

View File

@ -1,142 +0,0 @@
# Draft API
## Response
Single file:
```
{
"directory": "/image/2014/6f/w015i",
"type": "image",
"versions": {
"original": {
"filename": "original-15h1.png",
"height": 60,
"mime": "image/png",
"url": "/image/2014/6f/w015i/original-15h1.png",
"size": 3464,
"width": 53
},
"pic": {
"filename": "pic-15h1.png",
"height": 90,
"mime": "image/png",
"url": "/image/2014/6f/w015i/pic-15h1.png",
"size": 7648,
"width": 120
}
}
}
```
Multiple files:
```
{
"files": [
{...},
{...}
]
}
```
## Binary upload
```
POST /files HTTP/1.1
Content-Length: 21744
Accept: application/json
Content-Disposition: attachment; filename="pic.jpg"
...bytes...
```
## Multipart
```
POST /files HTTP/1.1
Content-Length: 21929
Accept: application/json
Content-Type: multipart/form-data; boundary=----5XhQf4IXV9Q26uHM
------5XhQf4IXV9Q26uHM
Content-Disposition: form-data; name="files[]"; filename="pic.jpg"
Content-Type: image/jpeg
...bytes...
```
## Chunked multipart
First request create temporary file
```
POST /files HTTP/1.1
Content-Length: 25185
Content-Range: bytes 0-24999/52097
Content-Disposition: attachment; filename="kino.jpg"
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAD3u12ABYZTJiIy3
------WebKitFormBoundaryAD3u12ABYZTJiIy3
Content-Disposition: form-data; name="files[]"; filename="kino.jpg"
Content-Type: image/jpeg
...bytes...
------WebKitFormBoundaryAD3u12ABYZTJiIy3--
```
Second request write chunk to exists temporary file
```
POST /files HTTP/1.1
Content-Length: 25185
Content-Range: bytes 25000-49999/52097
Content-Disposition: attachment; filename="kino.jpg"
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryvbE2anvAQyF3PWZS
------WebKitFormBoundaryvbE2anvAQyF3PWZS
Content-Disposition: form-data; name="files[]"; filename="kino.jpg"
Content-Type: image/jpeg
...bytes...
------WebKitFormBoundaryvbE2anvAQyF3PWZS--
```
Last request write chunk to exists temporary file, complete upload, create attachment.
```
POST /files HTTP/1.1
Content-Length: 2282
Content-Range: bytes 50000-52096/52097
Content-Disposition: attachment; filename="kino.jpg"
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrHBafxSExXodxlnL
------WebKitFormBoundaryrHBafxSExXodxlnL
Content-Disposition: form-data; name="files[]"; filename="kino.jpg"
Content-Type: image/jpeg
...bytes...
------WebKitFormBoundaryrHBafxSExXodxlnL--
```
## Chunked binary
```
POST /files HTTP/1.1
Content-Length: 10240
Content-Range: bytes 0-10239/36431
Accept: application/json
Content-Disposition: attachment; filename="pic.jpg"
Content-Type: image/jpeg
...bytes...
```
## Check chunked upload progress
```
PUT /files/some_url HTTP/1.1
Content-Length: 0
Content-Range: bytes */2000000
```
```
HTTP/1.1 308 Resume Incomplete
Content-Length: 0
Range: 0-42
```

View File

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Zaur Abasmirzoev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,73 +0,0 @@
## Coquelicot
Coquelicot is an easy to use server-side upload service written in Go.
It is compatible with the [jQuery-File-Upload](https://github.com/blueimp/jQuery-File-Upload)
widget and supports chunked and resumable file upload.
Using Coquelicot, you can add upload functionality to your project
very easily. Just download and unzip the Coquelicot binary distribution
for your OS and configure the jQuery-File-Upload widget.
![logo](http://go-tsunami.com/assets/images/coquelicotLogo.jpg)
### Server Setup
You can use a binary release or get the project if you have a working Go installation.
#### Binary Release
Grab the latest [binary release](https://github.com/gotsunami/coquelicot/releases) for you system. Unzip it
and run
```
$ ./coquelicot -storage /tmp/files -host localhost:9073
```
to store uploaded files into `/tmp/files` and make the application listen on the loopback interface port 9073
(run `coquelicot.exe` on Windows).
#### Source Release
Grab the latest stable version with:
```
$ go get gopkg.in/gotsunami/coquelicot.v1
```
See the [API documentation](http://gopkg.in/gotsunami/coquelicot.v1).
### jQuery-File-Upload Setup (Client)
The `fileupload` object needs the `xhrFields`, `maxChunkSize` and `add` fields to be defined.
- `xhrFields`: enables sending of cross-domain cookies, which is required to properly handle chunks of data server-side
- `maxChunkSize`: enables uploading chunks of file
- `add`: overwrites the default `add` handler to support resuming file upload
Download the [latest release](https://github.com/blueimp/jQuery-File-Upload/releases) of jQuery-File-Upload,
edit the `js/main.js` file in the distribution and make the `fileupload` initialization look like
(replacing the `localhost:9073` part with the name:port of your server running the `coquelicot` program):
```
$('#fileupload').fileupload({
// Send cross-domain cookies
xhrFields: {withCredentials: true},
url: 'http://localhost:9073/files',
// Chunk size in bytes
maxChunkSize: 1000000,
// Enable file resume
add: function (e, data) {
var that = this;
$.ajax({
url: 'http://localhost:9073/resume',
xhrFields: {withCredentials: true},
data: {file: data.files[0].name}
}).done(function(result) {
var file = result.file;
data.uploadedBytes = file && file.size;
$.blueimp.fileupload.prototype.options.add.call(that, e, data);
});
}
});
```

View File

@ -1,39 +0,0 @@
package coquelicot
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateAttachment(t *testing.T) {
assert := assert.New(t)
ofile := originalImageFile()
storage := dummy + "/root_storage"
converts := map[string]string{"original": "", "thumbnail": "120x80"}
attachment, err := create(storage, ofile, converts, false)
assert.Nil(err)
// Convert option not set
assert.Equal(len(attachment.Versions), 1)
data := attachment.ToJson()
assert.Equal(data["type"], "image")
}
func originalImageFile() *originalFile {
return &originalFile{
BaseMime: "image",
Filepath: dummy + "/32509211_news_bigpic.jpg",
Filename: "32509211_news_bigpic.jpg",
}
}
func originalPdfFile() *originalFile {
return &originalFile{
BaseMime: "application",
Filepath: dummy + "/Learning-Go-latest.pdf",
Filename: "Learning-Go-latest.pdf",
}
}

View File

@ -1,2 +0,0 @@
Build it with `./build.sh` which includes a version number (from git).

View File

@ -1,17 +0,0 @@
#!/bin/sh
# Writes a version file with the latest git commit id
# and any tag associated with it.
COMMIT=$(git log --format="%h" -n 1)
TAG=$(git describe --all --exact-match $COMMIT)
cat > version.go << EOF
package main
const (
appVersion = "$COMMIT $TAG"
)
EOF
go build && rm -f version.go

View File

@ -1,13 +0,0 @@
package main
import "flag"
var (
// Root for storage
storage = flag.String("storage", "./dummy/root_storage", "Root for storage")
// Host and port falco server
host = flag.String("host", "localhost:9073", "host:port for pavo server")
version = flag.Bool("version", false, "App version")
convert = flag.Bool("convert", false, "Use ImageMagick convert to create a thumbnail image")
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,39 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"gopkg.in/gotsunami/coquelicot.v1"
)
func main() {
flag.Parse()
if *version {
fmt.Printf("version: %s\n", appVersion)
return
}
s := coquelicot.NewStorage(*storage)
s.Option(coquelicot.Convert(*convert))
logger := log.New(os.Stdout, "", log.LstdFlags)
routes := map[string]http.HandlerFunc{
"/files": s.UploadHandler,
"/resume": s.ResumeHandler,
}
for path, handler := range routes {
http.Handle(path, coquelicot.Adapt(http.HandlerFunc(handler),
coquelicot.CORSMiddleware(),
coquelicot.LogMiddleware(logger)),
)
}
log.Printf("Storage place in: %s", s.StorageDir())
log.Printf("Start server on %s", *host)
log.Fatal(http.ListenAndServe(*host, nil))
}

View File

@ -1,43 +0,0 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestUploadBinary(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("POST", "/files", nil)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-File", "./dummy/bin-data")
req.Header.Set("Content-Disposition", `attachment; filename="basta.png"`)
req.AddCookie(&http.Cookie{Name: "coquelicot", Value: "abcdef"})
r := gin.Default()
r.POST("/files", CreateAttachment)
rw := httptest.NewRecorder()
r.ServeHTTP(rw, req)
assert.Equal(http.StatusCreated, rw.Code)
//var d map[string]interface{}
//json.Unmarshal(rw.Body.Bytes(), &d)
//t.Logf("json decode: %+v", d)
}
func TestGetConvertParams(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("POST", `/files?converts={"pic":"120x90"}`, nil)
convert, err := GetConvertParams(req)
assert.Nil(err)
assert.Equal("120x90", convert["pic"])
}

View File

@ -1,45 +0,0 @@
#!/bin/sh
# Writes a version file with the latest git commit id
# and any tag associated with it.
COMMIT=$(git log --format="%h" -n 1)
TAG=$(git describe --all --exact-match $COMMIT)
BIN=coquelicot
cat > version.go << EOF
package main
const (
appVersion = "$COMMIT $TAG"
)
EOF
echo "(use -a to build for all platforms (go >= 1.5)"
ARCH=amd64
OSLIST=$(uname -s|tr '[:upper:]' '[:lower:]')
if [ "$1" = "-a" ]; then
OSLIST="linux windows freebsd darwin"
fi
rm -f /tmp/$BIN*.zip
RTAG=$(git tag|tail -1)
if [ -z "$RTAG" ]; then
RTAG=$COMMIT
fi
for OS in $OSLIST; do
echo "Building for $OS ..."
TMP=/tmp/c$OS$ARCH
rm -rf $TMP
GOOS=$OS GOARCH=$ARCH go build -o $TMP/$BIN
if [ "$OS" = "windows" ]; then
mv $TMP/$BIN $TMP/$BIN.exe
fi
zip -j /tmp/$BIN-$RTAG-$OS-$ARCH.zip $TMP/$BIN* && rm -rf $TMP
done
rm -f version.go

View File

@ -1,18 +0,0 @@
// Package coquelicot provides (chunked) file upload capability (with resume).
package coquelicot
type Storage struct {
output string
verbosity int
}
// FIXME: global for now
var makeThumbnail bool
func (s *Storage) StorageDir() string {
return s.output
}
func NewStorage(rootDir string) *Storage {
return &Storage{output: rootDir}
}

View File

@ -1,19 +0,0 @@
package coquelicot
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPrepareDir(t *testing.T) {
assert := assert.New(t)
root := "dummy/root_storage"
dm, err := createDir(root, "image")
assert.Nil(err)
assert.Equal(root, dm.Root)
dm, err = checkDir(root, "/image/2014/2a/q1b12")
assert.Nil(err)
}

View File

@ -1,75 +0,0 @@
package coquelicot
import (
"fmt"
"os/exec"
)
type fileImageManager struct {
*fileDefaultManager
Width int
Height int
thumbnail bool // Add resized version with ImageMagick
}
// Save version from original with convert command-line tool.
func (fim *fileImageManager) convert(src string, convert string) error {
if !fim.thumbnail {
// Raw copy
return fim.rawCopy(src, convert)
}
err := convertImage(src, fim.Filepath(), convert)
if err != nil {
return err
}
fim.Width, fim.Height, fim.Size, err = identifyImageSizes(fim.Filepath())
if err != nil {
return err
}
return nil
}
func (fim *fileImageManager) ToJson() map[string]interface{} {
return map[string]interface{}{
"url": fim.Url(),
"filename": fim.Filename,
"size": fim.Size,
"width": fim.Width,
"height": fim.Height,
}
}
func convertImage(src, dest, convert string) error {
args := []string{src, "-strip"}
if convert != "" {
cv := []string{"-resize", convert + "^", "-gravity", "center", "-extent", convert}
args = append(args, cv...)
}
args = append(args, dest)
out, err := exec.Command("convert", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("Error move original: %s, %s", err, string(out))
}
return nil
}
func identifyImageSizes(filepath string) (int, int, int64, error) {
cmd := exec.Command("identify", "-format", `"%w:%h:%b"`, filepath)
out, err := cmd.CombinedOutput()
if err != nil {
return 0, 0, 0, fmt.Errorf("Identify Sizes: %s; detail: %s", err, string(out))
}
var w, h int
var s int64
fmt.Sscanf(string(out), `"%d:%d:%dB"`, &w, &h, &s)
return w, h, s, nil
}

View File

@ -1,113 +0,0 @@
package coquelicot
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)
type fileManager interface {
convert(string, string) error
SetFilename(*originalFile)
ToJson() map[string]interface{}
}
type fileBaseManager struct {
Dir *dirManager
Version string
Filename string
}
// Return fileManager for given base mime and version.
func newFileManager(dm *dirManager, mime_base, version string) fileManager {
fbm := &fileBaseManager{Dir: dm, Version: version}
fdm := &fileDefaultManager{fileBaseManager: fbm}
switch mime_base {
case "image":
return &fileImageManager{fileDefaultManager: fdm, thumbnail: makeThumbnail}
}
return fdm
}
func (fbm *fileBaseManager) SetFilename(file *originalFile) {
ext := filepath.Ext(file.Filename)
fbm.Filename = file.Filename[:len(file.Filename)-len(ext)] + "-" + fbm.Version + file.Ext()
if fbm.Version == "original" {
fbm.Filename = file.Filename
}
}
func (fbm *fileBaseManager) Filepath() string {
return filepath.Join(fbm.Dir.Abs(), fbm.Filename)
}
func (fbm *fileBaseManager) Url() string {
return filepath.Join(fbm.Dir.Path, fbm.Filename)
}
// copyFile copies a file from src to dst. If src and dst files exist, and are
// the same, then return success. Otherwise copy the file contents from src to dst.
func (fbm *fileBaseManager) copyFile(src, dst string) error {
sfi, err := os.Stat(src)
if err != nil {
return err
}
if !sfi.Mode().IsRegular() {
// cannot copy non-regular files (e.g., directories,
// symlinks, devices, etc.)
// FIXME
return fmt.Errorf("CopyFile: non-regular source file %s (%q)", sfi.Name(), sfi.Mode().String())
}
dfi, err := os.Stat(dst)
if err != nil {
if !os.IsNotExist(err) {
return err
}
} else {
if !(dfi.Mode().IsRegular()) {
return fmt.Errorf("CopyFile: non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
}
if os.SameFile(sfi, dfi) {
return err
}
}
if err := fbm.copyFileContents(src, dst); err != nil {
return err
}
return nil
}
// copyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
func (fbm *fileBaseManager) copyFileContents(src, dst string) error {
var err error
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
cerr := out.Close()
if err == nil {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return err
}
err = out.Sync()
return err
}
func seconds() int64 {
t := time.Now()
return int64(t.Hour()*3600 + t.Minute()*60 + t.Second())
}

View File

@ -1,32 +0,0 @@
package coquelicot
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestparseMeta(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("POST", "/files", nil)
req.Header.Set("Content-Type", "multipart/form-data; boundary=----Zam1WUeLK7vBj4wN")
req.Header.Set("Content-Range", "bytes 512000-1023999/1141216")
req.Header.Set("Content-Disposition", `attachment; filename="picture.jpg"`)
req.AddCookie(&http.Cookie{Name: "coquelicot", Value: "abcdef"})
meta, err := parseMeta(req)
assert.Nil(err)
assert.Equal(meta.MediaType, "multipart/form-data")
assert.Equal(meta.Boundary, "----Zam1WUeLK7vBj4wN")
assert.Equal(meta.Range.Start, int64(512000))
assert.Equal(meta.Range.End, int64(1023999))
assert.Equal(meta.Range.Size, int64(1141216))
assert.Equal(meta.Filename, "picture.jpg")
assert.Equal(meta.UploadSid, "abcdef")
}

View File

@ -1,50 +0,0 @@
package coquelicot
import (
"log"
"net/http"
)
type Adapter func(http.Handler) http.Handler
func Adapt(h http.Handler, adapters ...Adapter) http.Handler {
for k := len(adapters) - 1; k >= 0; k-- {
h = adapters[k](h)
}
return h
}
func CORSMiddleware() Adapter {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Content-Length, Accept-Encoding, Content-Range, Content-Disposition, Authorization")
// Since we need to support cross-domain cookies, we must support XHR requests
// with credentials, so the Access-Control-Allow-Credentials header is required
// and Access-Control-Allow-Origin cannot be equal to "*" but reply with the same Origin.
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS.
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
if r.Method == "OPTIONS" {
return
}
h.ServeHTTP(w, r)
})
}
}
func LogMiddleware(logger *log.Logger) Adapter {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r)
path := r.URL.Path
if len(r.URL.RawQuery) > 0 {
path += "?" + r.URL.RawQuery
}
logger.Printf("%s %s [%s]\n", r.Method, path, r.RemoteAddr)
})
}
}

View File

@ -1,23 +0,0 @@
package coquelicot
type option func(*Storage)
func (s *Storage) Option(opts ...option) {
for _, opt := range opts {
opt(s)
}
}
// Verbosity sets verbosity level (1 to 3).
func Verbosity(level int) option {
return func(s *Storage) {
s.verbosity = level
}
}
// Convert generates an image thumbnail using ImageMagick.
func Convert(b bool) option {
return func(s *Storage) {
makeThumbnail = b
}
}

View File

@ -1,231 +0,0 @@
package coquelicot
import (
"crypto/md5"
"encoding/hex"
"errors"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"strings"
)
// Error incomplete returned by uploader when loaded non-last chunk.
var incomplete = errors.New("incomplete")
// Structure describes the state of the original file.
type originalFile struct {
BaseMime string
Filepath string
Filename string
Size int64
}
func (ofile *originalFile) Ext() string {
return strings.ToLower(filepath.Ext(ofile.Filename))
}
// Downloading files from the received request.
// The root directory of storage, storage, used to temporarily store chunks.
// Returns an array of the original files and error.
// If you load a portion of the file, chunk, it will be stored in err error incomplete,
// and in an array of a single file. File size will fit the current size.
func process(req *http.Request, storage string) ([]*originalFile, error) {
meta, err := parseMeta(req)
if err != nil {
return nil, err
}
body, err := newBody(req.Header.Get("X-File"), req.Body)
if err != nil {
return nil, err
}
up := &uploader{Root: storage, Meta: meta, body: body}
files, err := up.SaveFiles()
if err == incomplete {
return files, err
}
if err != nil {
return nil, err
}
return files, nil
}
// Upload manager.
type uploader struct {
Root string
Meta *meta
body *body
}
// Function SaveFiles sequentially loads the original files or chunk's.
func (up *uploader) SaveFiles() ([]*originalFile, error) {
files := make([]*originalFile, 0)
for {
ofile, err := up.SaveFile()
if err == io.EOF {
break
}
if err == incomplete {
files = append(files, ofile)
return files, err
}
if err != nil {
return nil, err
}
files = append(files, ofile)
}
return files, nil
}
// Function loads one or download the original file chunk.
// Asks for the starting position in the body of the request to read the next file.
// Asks for a temporary file.
// Writes data from the request body into a temporary file.
// Specifies the size of the resulting temporary file.
// If the query specified header Content-Range,
// and the size of the resulting file does not match, it returns an error incomplete.
// Otherwise, defines the basic mime type, and returns the original file.
func (up *uploader) SaveFile() (*originalFile, error) {
body, filename, err := up.Reader()
if err != nil {
return nil, err
}
temp_file, err := up.tempFile()
if err != nil {
return nil, err
}
defer temp_file.Close()
if err = up.Write(temp_file, body); err != nil {
return nil, err
}
fi, err := temp_file.Stat()
if err != nil {
return nil, err
}
ofile := &originalFile{Filename: filename, Filepath: temp_file.Name(), Size: fi.Size()}
if up.Meta.Range != nil && ofile.Size != up.Meta.Range.Size {
return ofile, incomplete
}
ofile.BaseMime, err = identifyMime(ofile.Filepath)
if err != nil {
return nil, err
}
return ofile, nil
}
// Returns the reader to read the file or chunk of request body and the original file name.
// If the request header Content-Type is multipart/form-data, returns the next copy part.
// If all of part read the case of binary loading read the request body, an error is returned io.EOF.
func (up *uploader) Reader() (io.Reader, string, error) {
if up.Meta.MediaType == "multipart/form-data" {
if up.body.MR == nil {
up.body.MR = multipart.NewReader(up.body.body, up.Meta.Boundary)
}
for {
part, err := up.body.MR.NextPart()
if err != nil {
return nil, "", err
}
if part.FormName() == "files[]" {
return part, part.FileName(), nil
}
}
}
if !up.body.Available {
return nil, "", io.EOF
}
up.body.Available = false
return up.body.body, up.Meta.Filename, nil
}
// Returns a temporary file to download the file or resume chunk.
func (up *uploader) tempFile() (*os.File, error) {
if up.Meta.Range == nil {
return tempFile()
}
return tempFileChunks(up.Meta.Range.Start, up.Root, up.Meta.UploadSid, up.Meta.Filename)
}
// Returns the newly created temporary file.
func tempFile() (*os.File, error) {
return ioutil.TempFile(os.TempDir(), "coquelicot")
}
// Returns a temporary file to download chunk.
// To calculate a unique file name used cookie named coquelicot and the original file name.
// File located in the directory chunks storage root directory.
// Before returning the file pointer is shifted by the value of offset,
// in a situation where the pieces are loaded from the second to the last.
func tempFileChunks(offset int64, storage, upload_sid, user_filename string) (*os.File, error) {
hasher := md5.New()
hasher.Write([]byte(upload_sid + user_filename))
filename := hex.EncodeToString(hasher.Sum(nil))
path := filepath.Join(storage, "chunks")
err := os.MkdirAll(path, 0755)
if err != nil {
return nil, err
}
file, err := os.OpenFile(filepath.Join(path, filename), os.O_CREATE|os.O_WRONLY, 0664)
if err != nil {
return nil, err
}
if _, err = file.Seek(offset, 0); err != nil {
return nil, err
}
return file, nil
}
// The function writes a temporary file value from reader.
func (up *uploader) Write(temp_file *os.File, body io.Reader) error {
var err error
if up.Meta.Range == nil {
_, err = io.Copy(temp_file, body)
} else {
chunk_size := up.Meta.Range.End - up.Meta.Range.Start + 1
_, err = io.CopyN(temp_file, body, chunk_size)
}
return err
}
// identifyMine gets base mime type.
func identifyMime(file string) (string, error) {
f, err := os.Open(file)
if err != nil {
return "", err
}
defer f.Close()
// DetectContentType reads at most the first 512 bytes
buf := make([]byte, 512)
_, err = f.Read(buf)
if err != nil {
return "", err
}
mime := strings.Split(http.DetectContentType(buf), "/")[0]
return mime, nil
}

View File

@ -1,129 +0,0 @@
package coquelicot
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"testing"
"code.google.com/p/go-uuid/uuid"
"github.com/stretchr/testify/assert"
)
const dummy = "bin/coquelicot/dummy"
func TestUploadMultipart(t *testing.T) {
assert := assert.New(t)
var body bytes.Buffer
mw := multipart.NewWriter(&body)
if err := writeMPbody(dummy+"/32509211_news_bigpic.jpg", mw); err != nil {
assert.Error(err)
}
if err := writeMPbody(dummy+"/kino.jpg", mw); err != nil {
assert.Error(err)
}
mw.Close()
req, _ := http.NewRequest("POST", "/files", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.AddCookie(&http.Cookie{Name: "coquelicot", Value: "abcdef"})
files, err := process(req, dummy+"/root_storage")
assert.Nil(err)
assert.Equal("kino.jpg", files[1].Filename)
assert.Equal("image", files[1].BaseMime)
}
func TestUploadBinary(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("POST", "/files", nil)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-File", dummy+"/bin-data")
req.Header.Set("Content-Disposition", `attachment; filename="basta.png"`)
req.AddCookie(&http.Cookie{Name: "coquelicot", Value: "abcdef"})
files, err := process(req, dummy+"/root_storage")
assert.Nil(err)
assert.Equal("basta.png", files[0].Filename)
assert.Equal("image", files[0].BaseMime)
}
func TestUploadChunked(t *testing.T) {
assert := assert.New(t)
storage := dummy + "/root_storage"
fname := dummy + "/kino.jpg"
f, _ := os.Open(fname)
defer f.Close()
cookie := &http.Cookie{Name: "coquelicot", Value: uuid.New()}
req := createChunkRequest(f, 0, 24999)
req.AddCookie(cookie)
files, err := process(req, storage)
assert.Equal(incomplete, err)
assert.Equal(25000, int(files[0].Size))
req = createChunkRequest(f, 25000, 49999)
req.AddCookie(cookie)
files, err = process(req, storage)
assert.Equal(incomplete, err)
assert.Equal(50000, int(files[0].Size))
req = createChunkRequest(f, 50000, 52096)
req.AddCookie(cookie)
files, err = process(req, storage)
assert.Nil(err)
assert.Equal(52097, int(files[0].Size))
assert.Equal("kino.jpg", files[0].Filename)
}
func createChunkRequest(f *os.File, start int64, end int64) *http.Request {
var body bytes.Buffer
mw := multipart.NewWriter(&body)
fi, _ := f.Stat()
fw, _ := mw.CreateFormFile("files[]", fi.Name())
io.CopyN(fw, f, end-start+1)
mw.Close()
req, _ := http.NewRequest("POST", "/files", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Content-Disposition", `attachment; filename="`+fi.Name()+`"`)
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fi.Size()))
return req
}
func TestTempFileChunks(t *testing.T) {
assert := assert.New(t)
file, err := tempFileChunks(0, dummy+"/root_storage", "abcdef", "kino.jpg")
assert.Nil(err)
assert.NotNil(file)
}
func writeMPbody(fname string, mw *multipart.Writer) error {
fw, _ := mw.CreateFormFile("files[]", filepath.Base(fname))
f, err := os.Open(fname)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(fw, f)
if err != nil {
return err
}
return nil
}

View File

@ -1,144 +0,0 @@
package coquelicot
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path"
"time"
// "github.com/gin-gonic/gin"
"github.com/pborman/uuid"
httputil "github.com/goodrain/rainbond/util/http"
)
type H map[string]interface{}
func toJSON(w http.ResponseWriter, code int, obj interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
b, err := json.Marshal(obj)
if err != nil {
log.Println("error:", err.Error())
return
}
if _, err := w.Write(b); err != nil {
log.Println("error:", err.Error())
return
}
}
// ResumeHandler allows resuming a file upload.
//func (s *Storage) ResumeHandler(c *gin.Context) {
func (s *Storage) ResumeHandler(w http.ResponseWriter, r *http.Request) {
status := http.StatusOK
filename := r.URL.Query().Get("file")
cookie, _ := r.Cookie("coquelicot")
offset := int64(0)
if cookie != nil {
hasher := md5.New()
hasher.Write([]byte(cookie.Value + filename))
chunkname := hex.EncodeToString(hasher.Sum(nil))
fi, err := os.Stat(path.Join(s.output, "chunks", chunkname))
if err != nil {
if !os.IsNotExist(err) {
toJSON(w, http.StatusInternalServerError, H{
"status": http.StatusText(http.StatusInternalServerError),
"error": fmt.Sprintf("Resume error: %q", err.Error()),
})
return
}
} else {
offset = fi.Size()
}
}
toJSON(w, status, H{"status": http.StatusText(status), "file": H{"size": offset}})
}
// UploadHandler is the endpoint for uploading and storing files.
//func (s *Storage) UploadHandler(c *gin.Context) {
func (s *Storage) UploadHandler(w http.ResponseWriter, r *http.Request) {
converts, err := getConvertParams(r)
if err != nil {
toJSON(w, http.StatusBadRequest, H{
"status": "error",
"error": fmt.Sprintf("Query params: %s", err),
})
return
}
converts["original"] = ""
// File upload cookie so we can keep track of chunks.
cookie, _ := r.Cookie("coquelicot")
if cookie == nil {
cookie = &http.Cookie{
Name: "coquelicot",
Value: uuid.New(),
Expires: time.Now().Add(2 * 24 * time.Hour),
Path: "/",
}
r.AddCookie(cookie)
http.SetCookie(w, cookie)
}
// Performs the processing of writing data into chunk files.
files, err := process(r, s.StorageDir())
if err == incomplete {
toJSON(w, http.StatusOK, H{
"status": http.StatusText(http.StatusOK),
"file": H{"size": files[0].Size},
})
return
}
if err != nil {
toJSON(w, http.StatusBadRequest, H{
"status": http.StatusText(http.StatusBadRequest),
"error": fmt.Sprintf("Upload error: %q", err.Error()),
})
return
}
data := make([]map[string]interface{}, 0)
for _, ofile := range files {
println("1",ofile.Filename,"2",ofile.Filepath,"3",ofile.Size,"4",ofile.BaseMime)
// true to delete final chunk
_, err := create(s.StorageDir(), ofile, converts, true)
if err != nil {
data = append(data, map[string]interface{}{
"name": ofile.Filename,
"size": ofile.Size,
"error": err.Error(),
})
httputil.ReturnError(r,w,500,err.Error())
}
}
httputil.ReturnSuccess(r, w, nil)
}
// Get parameters for convert from Request query string
func getConvertParams(req *http.Request) (map[string]string, error) {
raw_converts := req.URL.Query().Get("converts")
if raw_converts == "" {
raw_converts = "{}"
}
convert := make(map[string]string)
err := json.Unmarshal([]byte(raw_converts), &convert)
if err != nil {
return nil, err
}
return convert, nil
}

View File

@ -1,4 +1,4 @@
package coquelicot
package upload
import (
"os"
@ -11,9 +11,7 @@ type attachment struct {
Versions map[string]fileManager
}
// Function receive root directory, original file, convertion parameters.
// Return attachment saved. The final chunk is deleted if delChunk is true.
func create(storage string, ofile *originalFile, converts map[string]string, delChunk bool) (*attachment, error) {
func create(storage string, ofile *originalFile, delChunk bool) (*attachment, error) {
dm, err := createDir(storage, ofile.BaseMime)
if err != nil {
return nil, err
@ -25,12 +23,9 @@ func create(storage string, ofile *originalFile, converts map[string]string, del
Versions: make(map[string]fileManager),
}
if ofile.BaseMime == "image" {
converts["thumbnail"] = "120x90"
}
makeVersion := func(a *attachment, version, convert string) error {
fm, err := at.createVersion(version, convert)
fm, err := at.createVersion(version)
if err != nil {
return err
}
@ -42,12 +37,6 @@ func create(storage string, ofile *originalFile, converts map[string]string, del
return nil, err
}
if makeThumbnail {
if err := makeVersion(at, "thumbnail", converts["thumbnail"]); err != nil {
return nil, err
}
}
if delChunk {
return at, os.Remove(at.originalFile.Filepath)
}
@ -55,11 +44,11 @@ func create(storage string, ofile *originalFile, converts map[string]string, del
}
// Directly save single version and return fileManager.
func (attachment *attachment) createVersion(version string, convert string) (fileManager, error) {
fm := newFileManager(attachment.Dir, attachment.originalFile.BaseMime, version)
func (attachment *attachment) createVersion(version string) (fileManager, error) {
fm := newFileManager(attachment.Dir, version)
fm.SetFilename(attachment.originalFile)
if err := fm.convert(attachment.originalFile.Filepath, convert); err != nil {
if err := fm.convert(attachment.originalFile.Filepath); err != nil {
return nil, err
}

View File

@ -1,7 +1,6 @@
package coquelicot
package upload
import (
"bufio"
"io"
"mime/multipart"
"os"
@ -16,17 +15,8 @@ type body struct {
}
// Check exists body in xfile and return body.
func newBody(xfile string, req_body io.Reader) (*body, error) {
if xfile == "" {
func newBody(req_body io.Reader) (*body, error) {
return &body{body: req_body, Available: true}, nil
}
fh, err := os.Open(xfile)
if err != nil {
return nil, err
}
return &body{XFile: fh, body: bufio.NewReader(fh), Available: true}, nil
}
// Close filehandler of body if XFile exists.

View File

@ -1,10 +1,6 @@
package coquelicot
package upload
import (
"crypto/sha1"
"errors"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
@ -30,17 +26,6 @@ func createDir(root, mime string) (*dirManager, error) {
return dm, nil
}
// Check path and return dirManager.
func checkDir(root, path string) (*dirManager, error) {
dm := newDirManager(root)
if m, _ := filepath.Match("/[a-z]*/[0-9]*/[0-9a-z]*/[0-9a-z]*", path); m != true {
return nil, errors.New("dir: path does not match the pattern")
}
dm.Path = path
return dm, nil
}
// newDirManager returns a new dirManager given a root.
func newDirManager(root string) *dirManager {
@ -59,9 +44,6 @@ func (dm *dirManager) create() error {
// Generate path given mime and date.
func (dm *dirManager) CalcPath(mime string) {
h := sha1.New()
io.WriteString(h, fmt.Sprintf("%d", time.Now().UnixNano()))
//dm.Path = fmt.Sprintf("/%x", h.Sum(nil))
dm.Path = ""
}

View File

@ -1,4 +1,4 @@
package coquelicot
package upload
import (
"os"
@ -9,8 +9,8 @@ type fileDefaultManager struct {
Size int64
}
func (fdm *fileDefaultManager) convert(src string, convert string) error {
return fdm.rawCopy(src, convert)
func (fdm *fileDefaultManager) convert(src string) error {
return fdm.rawCopy(src)
}
func (fdm *fileDefaultManager) ToJson() map[string]interface{} {
@ -21,7 +21,7 @@ func (fdm *fileDefaultManager) ToJson() map[string]interface{} {
}
}
func (fdm *fileDefaultManager) rawCopy(src, convert string) error {
func (fdm *fileDefaultManager) rawCopy(src string) error {
if err := fdm.copyFile(src, fdm.Filepath()); err != nil {
return err
}

View File

@ -1,4 +1,4 @@
package coquelicot
package upload
import (
"fmt"
@ -9,7 +9,7 @@ import (
)
type fileManager interface {
convert(string, string) error
convert(string) error
SetFilename(*originalFile)
ToJson() map[string]interface{}
}
@ -21,22 +21,14 @@ type fileBaseManager struct {
}
// Return fileManager for given base mime and version.
func newFileManager(dm *dirManager, mime_base, version string) fileManager {
func newFileManager(dm *dirManager, version string) fileManager {
fbm := &fileBaseManager{Dir: dm, Version: version}
fdm := &fileDefaultManager{fileBaseManager: fbm}
switch mime_base {
case "image":
return &fileImageManager{fileDefaultManager: fdm, thumbnail: makeThumbnail}
}
return fdm
}
func (fbm *fileBaseManager) SetFilename(file *originalFile) {
ext := filepath.Ext(file.Filename)
fbm.Filename = file.Filename[:len(file.Filename)-len(ext)] + "-" + fbm.Version + file.Ext()
if fbm.Version == "original" {
fbm.Filename = file.Filename
}
}
func (fbm *fileBaseManager) Filepath() string {

View File

@ -1,8 +1,7 @@
package coquelicot
package upload
import (
"errors"
"fmt"
"mime"
"net/http"
)
@ -11,15 +10,7 @@ import (
type meta struct {
MediaType string
Boundary string
Range *dataRange
Filename string
UploadSid string
}
type dataRange struct {
Start int64
End int64
Size int64
}
// Parse request headers and make Meta.
@ -30,22 +21,10 @@ func parseMeta(req *http.Request) (*meta, error) {
return nil, err
}
if err := m.parseContentRange(req.Header.Get("Content-Range")); err != nil {
return nil, err
}
if err := m.parseContentDisposition(req.Header.Get("Content-Disposition")); err != nil {
return nil, err
}
cookie, err := req.Cookie("coquelicot")
if err != nil {
return nil, err
}
if cookie != nil {
m.UploadSid = cookie.Value
}
return m, nil
}
@ -75,22 +54,6 @@ func (m *meta) parseContentType(ct string) error {
return nil
}
func (m *meta) parseContentRange(cr string) error {
if cr == "" {
return nil
}
var start, end, size int64
_, err := fmt.Sscanf(cr, "bytes %d-%d/%d", &start, &end, &size)
if err != nil {
return err
}
m.Range = &dataRange{Start: start, End: end, Size: size}
return nil
}
func (m *meta) parseContentDisposition(cd string) error {
if cd == "" {

View File

@ -1,4 +1,4 @@
package coquelicot
package upload
import (
"crypto/md5"
@ -28,18 +28,14 @@ func (ofile *originalFile) Ext() string {
return strings.ToLower(filepath.Ext(ofile.Filename))
}
// Downloading files from the received request.
// The root directory of storage, storage, used to temporarily store chunks.
// Returns an array of the original files and error.
// If you load a portion of the file, chunk, it will be stored in err error incomplete,
// and in an array of a single file. File size will fit the current size.
func process(req *http.Request, storage string) ([]*originalFile, error) {
meta, err := parseMeta(req)
if err != nil {
return nil, err
}
body, err := newBody(req.Header.Get("X-File"), req.Body)
body, err := newBody(req.Body)
if err != nil {
return nil, err
}
@ -71,12 +67,6 @@ func (up *uploader) SaveFiles() ([]*originalFile, error) {
if err == io.EOF {
break
}
if err == incomplete {
files = append(files, ofile)
return files, err
}
if err != nil {
return nil, err
}
@ -118,10 +108,6 @@ func (up *uploader) SaveFile() (*originalFile, error) {
ofile := &originalFile{Filename: filename, Filepath: temp_file.Name(), Size: fi.Size()}
if up.Meta.Range != nil && ofile.Size != up.Meta.Range.Size {
return ofile, incomplete
}
ofile.BaseMime, err = identifyMime(ofile.Filepath)
if err != nil {
return nil, err
@ -160,10 +146,7 @@ func (up *uploader) Reader() (io.Reader, string, error) {
// Returns a temporary file to download the file or resume chunk.
func (up *uploader) tempFile() (*os.File, error) {
if up.Meta.Range == nil {
return tempFile()
}
return tempFileChunks(up.Meta.Range.Start, up.Root, up.Meta.UploadSid, up.Meta.Filename)
return tempFile()
}
// Returns the newly created temporary file.
@ -203,12 +186,7 @@ func tempFileChunks(offset int64, storage, upload_sid, user_filename string) (*o
// The function writes a temporary file value from reader.
func (up *uploader) Write(temp_file *os.File, body io.Reader) error {
var err error
if up.Meta.Range == nil {
_, err = io.Copy(temp_file, body)
} else {
chunk_size := up.Meta.Range.End - up.Meta.Range.Start + 1
_, err = io.CopyN(temp_file, body, chunk_size)
}
_, err = io.Copy(temp_file, body)
return err
}

View File

@ -0,0 +1,48 @@
package upload
import (
"net/http"
httputil "github.com/goodrain/rainbond/util/http"
)
type Storage struct {
output string
verbosity int
}
func (s *Storage) StorageDir() string {
return s.output
}
func NewStorage(rootDir string) *Storage {
return &Storage{output: rootDir}
}
// UploadHandler is the endpoint for uploading and storing files.
func (s *Storage) UploadHandler(w http.ResponseWriter, r *http.Request) {
// Performs the processing of writing data into chunk files.
files, err := process(r, s.StorageDir())
if err == incomplete {
httputil.ReturnSuccess(r, w, nil)
return
}
if err != nil {
httputil.ReturnError(r, w, 500, err.Error())
return
}
data := make([]map[string]interface{}, 0)
for _, file := range files {
attachment, err := create(s.StorageDir(), file, true)
if err != nil {
httputil.ReturnError(r, w, 500, err.Error())
return
}
data = append(data, attachment.ToJson())
}
httputil.ReturnSuccess(r, w, data)
}

View File

@ -1,4 +0,0 @@
*.swp
tags
bin/coquelicot/coquelicot
bin/coquelicot/dummy/root_storage/*

View File

@ -1,142 +0,0 @@
# Draft API
## Response
Single file:
```
{
"directory": "/image/2014/6f/w015i",
"type": "image",
"versions": {
"original": {
"filename": "original-15h1.png",
"height": 60,
"mime": "image/png",
"url": "/image/2014/6f/w015i/original-15h1.png",
"size": 3464,
"width": 53
},
"pic": {
"filename": "pic-15h1.png",
"height": 90,
"mime": "image/png",
"url": "/image/2014/6f/w015i/pic-15h1.png",
"size": 7648,
"width": 120
}
}
}
```
Multiple files:
```
{
"files": [
{...},
{...}
]
}
```
## Binary upload
```
POST /files HTTP/1.1
Content-Length: 21744
Accept: application/json
Content-Disposition: attachment; filename="pic.jpg"
...bytes...
```
## Multipart
```
POST /files HTTP/1.1
Content-Length: 21929
Accept: application/json
Content-Type: multipart/form-data; boundary=----5XhQf4IXV9Q26uHM
------5XhQf4IXV9Q26uHM
Content-Disposition: form-data; name="files[]"; filename="pic.jpg"
Content-Type: image/jpeg
...bytes...
```
## Chunked multipart
First request create temporary file
```
POST /files HTTP/1.1
Content-Length: 25185
Content-Range: bytes 0-24999/52097
Content-Disposition: attachment; filename="kino.jpg"
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryAD3u12ABYZTJiIy3
------WebKitFormBoundaryAD3u12ABYZTJiIy3
Content-Disposition: form-data; name="files[]"; filename="kino.jpg"
Content-Type: image/jpeg
...bytes...
------WebKitFormBoundaryAD3u12ABYZTJiIy3--
```
Second request write chunk to exists temporary file
```
POST /files HTTP/1.1
Content-Length: 25185
Content-Range: bytes 25000-49999/52097
Content-Disposition: attachment; filename="kino.jpg"
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryvbE2anvAQyF3PWZS
------WebKitFormBoundaryvbE2anvAQyF3PWZS
Content-Disposition: form-data; name="files[]"; filename="kino.jpg"
Content-Type: image/jpeg
...bytes...
------WebKitFormBoundaryvbE2anvAQyF3PWZS--
```
Last request write chunk to exists temporary file, complete upload, create attachment.
```
POST /files HTTP/1.1
Content-Length: 2282
Content-Range: bytes 50000-52096/52097
Content-Disposition: attachment; filename="kino.jpg"
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryrHBafxSExXodxlnL
------WebKitFormBoundaryrHBafxSExXodxlnL
Content-Disposition: form-data; name="files[]"; filename="kino.jpg"
Content-Type: image/jpeg
...bytes...
------WebKitFormBoundaryrHBafxSExXodxlnL--
```
## Chunked binary
```
POST /files HTTP/1.1
Content-Length: 10240
Content-Range: bytes 0-10239/36431
Accept: application/json
Content-Disposition: attachment; filename="pic.jpg"
Content-Type: image/jpeg
...bytes...
```
## Check chunked upload progress
```
PUT /files/some_url HTTP/1.1
Content-Length: 0
Content-Range: bytes */2000000
```
```
HTTP/1.1 308 Resume Incomplete
Content-Length: 0
Range: 0-42
```

View File

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 Zaur Abasmirzoev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,73 +0,0 @@
## Coquelicot
Coquelicot is an easy to use server-side upload service written in Go.
It is compatible with the [jQuery-File-Upload](https://github.com/blueimp/jQuery-File-Upload)
widget and supports chunked and resumable file upload.
Using Coquelicot, you can add upload functionality to your project
very easily. Just download and unzip the Coquelicot binary distribution
for your OS and configure the jQuery-File-Upload widget.
![logo](http://go-tsunami.com/assets/images/coquelicotLogo.jpg)
### Server Setup
You can use a binary release or get the project if you have a working Go installation.
#### Binary Release
Grab the latest [binary release](https://github.com/gotsunami/coquelicot/releases) for you system. Unzip it
and run
```
$ ./coquelicot -storage /tmp/files -host localhost:9073
```
to store uploaded files into `/tmp/files` and make the application listen on the loopback interface port 9073
(run `coquelicot.exe` on Windows).
#### Source Release
Grab the latest stable version with:
```
$ go get gopkg.in/gotsunami/coquelicot.v1
```
See the [API documentation](http://gopkg.in/gotsunami/coquelicot.v1).
### jQuery-File-Upload Setup (Client)
The `fileupload` object needs the `xhrFields`, `maxChunkSize` and `add` fields to be defined.
- `xhrFields`: enables sending of cross-domain cookies, which is required to properly handle chunks of data server-side
- `maxChunkSize`: enables uploading chunks of file
- `add`: overwrites the default `add` handler to support resuming file upload
Download the [latest release](https://github.com/blueimp/jQuery-File-Upload/releases) of jQuery-File-Upload,
edit the `js/main.js` file in the distribution and make the `fileupload` initialization look like
(replacing the `localhost:9073` part with the name:port of your server running the `coquelicot` program):
```
$('#fileupload').fileupload({
// Send cross-domain cookies
xhrFields: {withCredentials: true},
url: 'http://localhost:9073/files',
// Chunk size in bytes
maxChunkSize: 1000000,
// Enable file resume
add: function (e, data) {
var that = this;
$.ajax({
url: 'http://localhost:9073/resume',
xhrFields: {withCredentials: true},
data: {file: data.files[0].name}
}).done(function(result) {
var file = result.file;
data.uploadedBytes = file && file.size;
$.blueimp.fileupload.prototype.options.add.call(that, e, data);
});
}
});
```

View File

@ -1,81 +0,0 @@
package coquelicot
import (
"os"
)
// attachment contain info about directory, base mime type and all files saved.
type attachment struct {
originalFile *originalFile
Dir *dirManager
Versions map[string]fileManager
}
// Function receive root directory, original file, convertion parameters.
// Return attachment saved. The final chunk is deleted if delChunk is true.
func create(storage string, ofile *originalFile, converts map[string]string, delChunk bool) (*attachment, error) {
dm, err := createDir(storage, ofile.BaseMime)
if err != nil {
return nil, err
}
at := &attachment{
originalFile: ofile,
Dir: dm,
Versions: make(map[string]fileManager),
}
if ofile.BaseMime == "image" {
converts["thumbnail"] = "120x90"
}
makeVersion := func(a *attachment, version, convert string) error {
fm, err := at.createVersion(version, convert)
if err != nil {
return err
}
at.Versions[version] = fm
return nil
}
if err := makeVersion(at, "original", ""); err != nil {
return nil, err
}
if makeThumbnail {
if err := makeVersion(at, "thumbnail", converts["thumbnail"]); err != nil {
return nil, err
}
}
if delChunk {
return at, os.Remove(at.originalFile.Filepath)
}
return at, nil
}
// Directly save single version and return fileManager.
func (attachment *attachment) createVersion(version string, convert string) (fileManager, error) {
fm := newFileManager(attachment.Dir, attachment.originalFile.BaseMime, version)
fm.SetFilename(attachment.originalFile)
if err := fm.convert(attachment.originalFile.Filepath, convert); err != nil {
return nil, err
}
return fm, nil
}
func (attachment *attachment) ToJson() map[string]interface{} {
data := make(map[string]interface{})
data["type"] = attachment.originalFile.BaseMime
data["dir"] = attachment.Dir.Path
data["name"] = attachment.originalFile.Filename
versions := make(map[string]interface{})
for version, fm := range attachment.Versions {
versions[version] = fm.ToJson()
}
data["versions"] = versions
return data
}

View File

@ -1,39 +0,0 @@
package coquelicot
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCreateAttachment(t *testing.T) {
assert := assert.New(t)
ofile := originalImageFile()
storage := dummy + "/root_storage"
converts := map[string]string{"original": "", "thumbnail": "120x80"}
attachment, err := create(storage, ofile, converts, false)
assert.Nil(err)
// Convert option not set
assert.Equal(len(attachment.Versions), 1)
data := attachment.ToJson()
assert.Equal(data["type"], "image")
}
func originalImageFile() *originalFile {
return &originalFile{
BaseMime: "image",
Filepath: dummy + "/32509211_news_bigpic.jpg",
Filename: "32509211_news_bigpic.jpg",
}
}
func originalPdfFile() *originalFile {
return &originalFile{
BaseMime: "application",
Filepath: dummy + "/Learning-Go-latest.pdf",
Filename: "Learning-Go-latest.pdf",
}
}

View File

@ -1,2 +0,0 @@
Build it with `./build.sh` which includes a version number (from git).

View File

@ -1,17 +0,0 @@
#!/bin/sh
# Writes a version file with the latest git commit id
# and any tag associated with it.
COMMIT=$(git log --format="%h" -n 1)
TAG=$(git describe --all --exact-match $COMMIT)
cat > version.go << EOF
package main
const (
appVersion = "$COMMIT $TAG"
)
EOF
go build && rm -f version.go

View File

@ -1,13 +0,0 @@
package main
import "flag"
var (
// Root for storage
storage = flag.String("storage", "./dummy/root_storage", "Root for storage")
// Host and port falco server
host = flag.String("host", "localhost:9073", "host:port for pavo server")
version = flag.Bool("version", false, "App version")
convert = flag.Bool("convert", false, "Use ImageMagick convert to create a thumbnail image")
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

View File

@ -1,39 +0,0 @@
package main
import (
"flag"
"fmt"
"log"
"net/http"
"os"
"gopkg.in/gotsunami/coquelicot.v1"
)
func main() {
flag.Parse()
if *version {
fmt.Printf("version: %s\n", appVersion)
return
}
s := coquelicot.NewStorage(*storage)
s.Option(coquelicot.Convert(*convert))
logger := log.New(os.Stdout, "", log.LstdFlags)
routes := map[string]http.HandlerFunc{
"/files": s.UploadHandler,
"/resume": s.ResumeHandler,
}
for path, handler := range routes {
http.Handle(path, coquelicot.Adapt(http.HandlerFunc(handler),
coquelicot.CORSMiddleware(),
coquelicot.LogMiddleware(logger)),
)
}
log.Printf("Storage place in: %s", s.StorageDir())
log.Printf("Start server on %s", *host)
log.Fatal(http.ListenAndServe(*host, nil))
}

View File

@ -1,43 +0,0 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
)
func TestUploadBinary(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("POST", "/files", nil)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-File", "./dummy/bin-data")
req.Header.Set("Content-Disposition", `attachment; filename="basta.png"`)
req.AddCookie(&http.Cookie{Name: "coquelicot", Value: "abcdef"})
r := gin.Default()
r.POST("/files", CreateAttachment)
rw := httptest.NewRecorder()
r.ServeHTTP(rw, req)
assert.Equal(http.StatusCreated, rw.Code)
//var d map[string]interface{}
//json.Unmarshal(rw.Body.Bytes(), &d)
//t.Logf("json decode: %+v", d)
}
func TestGetConvertParams(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("POST", `/files?converts={"pic":"120x90"}`, nil)
convert, err := GetConvertParams(req)
assert.Nil(err)
assert.Equal("120x90", convert["pic"])
}

View File

@ -1,45 +0,0 @@
#!/bin/sh
# Writes a version file with the latest git commit id
# and any tag associated with it.
COMMIT=$(git log --format="%h" -n 1)
TAG=$(git describe --all --exact-match $COMMIT)
BIN=coquelicot
cat > version.go << EOF
package main
const (
appVersion = "$COMMIT $TAG"
)
EOF
echo "(use -a to build for all platforms (go >= 1.5)"
ARCH=amd64
OSLIST=$(uname -s|tr '[:upper:]' '[:lower:]')
if [ "$1" = "-a" ]; then
OSLIST="linux windows freebsd darwin"
fi
rm -f /tmp/$BIN*.zip
RTAG=$(git tag|tail -1)
if [ -z "$RTAG" ]; then
RTAG=$COMMIT
fi
for OS in $OSLIST; do
echo "Building for $OS ..."
TMP=/tmp/c$OS$ARCH
rm -rf $TMP
GOOS=$OS GOARCH=$ARCH go build -o $TMP/$BIN
if [ "$OS" = "windows" ]; then
mv $TMP/$BIN $TMP/$BIN.exe
fi
zip -j /tmp/$BIN-$RTAG-$OS-$ARCH.zip $TMP/$BIN* && rm -rf $TMP
done
rm -f version.go

View File

@ -1,39 +0,0 @@
package coquelicot
import (
"bufio"
"io"
"mime/multipart"
"os"
)
// Upload body info.
type body struct {
XFile *os.File
body io.Reader
MR *multipart.Reader
Available bool
}
// Check exists body in xfile and return body.
func newBody(xfile string, req_body io.Reader) (*body, error) {
if xfile == "" {
return &body{body: req_body, Available: true}, nil
}
fh, err := os.Open(xfile)
if err != nil {
return nil, err
}
return &body{XFile: fh, body: bufio.NewReader(fh), Available: true}, nil
}
// Close filehandler of body if XFile exists.
func (body *body) Close() error {
if body.XFile != nil {
return body.XFile.Close()
}
return nil
}

View File

@ -1,18 +0,0 @@
// Package coquelicot provides (chunked) file upload capability (with resume).
package coquelicot
type Storage struct {
output string
verbosity int
}
// FIXME: global for now
var makeThumbnail bool
func (s *Storage) StorageDir() string {
return s.output
}
func NewStorage(rootDir string) *Storage {
return &Storage{output: rootDir}
}

View File

@ -1,76 +0,0 @@
package coquelicot
import (
"crypto/sha1"
"errors"
"fmt"
"io"
"math/rand"
"os"
"path/filepath"
"strconv"
"time"
)
// Directory mananger
type dirManager struct {
Root string
Path string
}
// Prepare dirManager given root, mime.
func createDir(root, mime string) (*dirManager, error) {
dm := newDirManager(root)
dm.CalcPath(mime)
if err := dm.create(); err != nil {
return nil, err
}
return dm, nil
}
// Check path and return dirManager.
func checkDir(root, path string) (*dirManager, error) {
dm := newDirManager(root)
if m, _ := filepath.Match("/[a-z]*/[0-9]*/[0-9a-z]*/[0-9a-z]*", path); m != true {
return nil, errors.New("dir: path does not match the pattern")
}
dm.Path = path
return dm, nil
}
// newDirManager returns a new dirManager given a root.
func newDirManager(root string) *dirManager {
return &dirManager{Root: root}
}
// Return absolute path for directory
func (dm *dirManager) Abs() string {
return filepath.Join(dm.Root, dm.Path)
}
// Create directory obtained by concatenating the root and path.
func (dm *dirManager) create() error {
return os.MkdirAll(dm.Root+dm.Path, 0755)
}
// Generate path given mime and date.
func (dm *dirManager) CalcPath(mime string) {
h := sha1.New()
io.WriteString(h, fmt.Sprintf("%d", time.Now().UnixNano()))
dm.Path = fmt.Sprintf("/%x", h.Sum(nil))
}
func yearDay(t time.Time) string {
return strconv.FormatInt(int64(t.YearDay()), 36)
}
func containerName(t time.Time) string {
r := rand.New(rand.NewSource(time.Now().UnixNano())).Intn(1000)
seconds := t.Hour()*3600 + t.Minute()*60 + t.Second()
return strconv.FormatInt(int64(seconds*1000+r), 36)
}

View File

@ -1,19 +0,0 @@
package coquelicot
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPrepareDir(t *testing.T) {
assert := assert.New(t)
root := "dummy/root_storage"
dm, err := createDir(root, "image")
assert.Nil(err)
assert.Equal(root, dm.Root)
dm, err = checkDir(root, "/image/2014/2a/q1b12")
assert.Nil(err)
}

View File

@ -1,40 +0,0 @@
package coquelicot
import (
"os"
)
type fileDefaultManager struct {
*fileBaseManager
Size int64
}
func (fdm *fileDefaultManager) convert(src string, convert string) error {
return fdm.rawCopy(src, convert)
}
func (fdm *fileDefaultManager) ToJson() map[string]interface{} {
return map[string]interface{}{
"url": fdm.Url(),
"filename": fdm.Filename,
"size": fdm.Size,
}
}
func (fdm *fileDefaultManager) rawCopy(src, convert string) error {
if err := fdm.copyFile(src, fdm.Filepath()); err != nil {
return err
}
f, err := os.Open(fdm.Filepath())
if err != nil {
return err
}
fi, err := f.Stat()
if err != nil {
return err
}
fdm.Size = fi.Size()
return nil
}

View File

@ -1,75 +0,0 @@
package coquelicot
import (
"fmt"
"os/exec"
)
type fileImageManager struct {
*fileDefaultManager
Width int
Height int
thumbnail bool // Add resized version with ImageMagick
}
// Save version from original with convert command-line tool.
func (fim *fileImageManager) convert(src string, convert string) error {
if !fim.thumbnail {
// Raw copy
return fim.rawCopy(src, convert)
}
err := convertImage(src, fim.Filepath(), convert)
if err != nil {
return err
}
fim.Width, fim.Height, fim.Size, err = identifyImageSizes(fim.Filepath())
if err != nil {
return err
}
return nil
}
func (fim *fileImageManager) ToJson() map[string]interface{} {
return map[string]interface{}{
"url": fim.Url(),
"filename": fim.Filename,
"size": fim.Size,
"width": fim.Width,
"height": fim.Height,
}
}
func convertImage(src, dest, convert string) error {
args := []string{src, "-strip"}
if convert != "" {
cv := []string{"-resize", convert + "^", "-gravity", "center", "-extent", convert}
args = append(args, cv...)
}
args = append(args, dest)
out, err := exec.Command("convert", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("Error move original: %s, %s", err, string(out))
}
return nil
}
func identifyImageSizes(filepath string) (int, int, int64, error) {
cmd := exec.Command("identify", "-format", `"%w:%h:%b"`, filepath)
out, err := cmd.CombinedOutput()
if err != nil {
return 0, 0, 0, fmt.Errorf("Identify Sizes: %s; detail: %s", err, string(out))
}
var w, h int
var s int64
fmt.Sscanf(string(out), `"%d:%d:%dB"`, &w, &h, &s)
return w, h, s, nil
}

View File

@ -1,113 +0,0 @@
package coquelicot
import (
"errors"
"fmt"
"mime"
"net/http"
)
// Info about request headers
type meta struct {
MediaType string
Boundary string
Range *dataRange
Filename string
UploadSid string
}
type dataRange struct {
Start int64
End int64
Size int64
}
// Parse request headers and make Meta.
func parseMeta(req *http.Request) (*meta, error) {
m := &meta{}
if err := m.parseContentType(req.Header.Get("Content-Type")); err != nil {
return nil, err
}
if err := m.parseContentRange(req.Header.Get("Content-Range")); err != nil {
return nil, err
}
if err := m.parseContentDisposition(req.Header.Get("Content-Disposition")); err != nil {
return nil, err
}
cookie, err := req.Cookie("coquelicot")
if err != nil {
return nil, err
}
if cookie != nil {
m.UploadSid = cookie.Value
}
return m, nil
}
func (m *meta) parseContentType(ct string) error {
if ct == "" {
m.MediaType = "application/octet-stream"
return nil
}
mediatype, params, err := mime.ParseMediaType(ct)
if err != nil {
return err
}
if mediatype == "multipart/form-data" {
boundary, ok := params["boundary"]
if !ok {
return errors.New("meta: boundary not defined")
}
m.MediaType = mediatype
m.Boundary = boundary
} else {
m.MediaType = "application/octet-stream"
}
return nil
}
func (m *meta) parseContentRange(cr string) error {
if cr == "" {
return nil
}
var start, end, size int64
_, err := fmt.Sscanf(cr, "bytes %d-%d/%d", &start, &end, &size)
if err != nil {
return err
}
m.Range = &dataRange{Start: start, End: end, Size: size}
return nil
}
func (m *meta) parseContentDisposition(cd string) error {
if cd == "" {
return nil
}
_, params, err := mime.ParseMediaType(cd)
if err != nil {
return err
}
filename, ok := params["filename"]
if !ok {
return errors.New("meta: filename in Content-Disposition not defined")
}
m.Filename = filename
return nil
}

View File

@ -1,32 +0,0 @@
package coquelicot
import (
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestparseMeta(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("POST", "/files", nil)
req.Header.Set("Content-Type", "multipart/form-data; boundary=----Zam1WUeLK7vBj4wN")
req.Header.Set("Content-Range", "bytes 512000-1023999/1141216")
req.Header.Set("Content-Disposition", `attachment; filename="picture.jpg"`)
req.AddCookie(&http.Cookie{Name: "coquelicot", Value: "abcdef"})
meta, err := parseMeta(req)
assert.Nil(err)
assert.Equal(meta.MediaType, "multipart/form-data")
assert.Equal(meta.Boundary, "----Zam1WUeLK7vBj4wN")
assert.Equal(meta.Range.Start, int64(512000))
assert.Equal(meta.Range.End, int64(1023999))
assert.Equal(meta.Range.Size, int64(1141216))
assert.Equal(meta.Filename, "picture.jpg")
assert.Equal(meta.UploadSid, "abcdef")
}

View File

@ -1,50 +0,0 @@
package coquelicot
import (
"log"
"net/http"
)
type Adapter func(http.Handler) http.Handler
func Adapt(h http.Handler, adapters ...Adapter) http.Handler {
for k := len(adapters) - 1; k >= 0; k-- {
h = adapters[k](h)
}
return h
}
func CORSMiddleware() Adapter {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, PATCH, DELETE")
w.Header().Set("Access-Control-Allow-Headers",
"Content-Type, Content-Length, Accept-Encoding, Content-Range, Content-Disposition, Authorization")
// Since we need to support cross-domain cookies, we must support XHR requests
// with credentials, so the Access-Control-Allow-Credentials header is required
// and Access-Control-Allow-Origin cannot be equal to "*" but reply with the same Origin.
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS.
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin"))
if r.Method == "OPTIONS" {
return
}
h.ServeHTTP(w, r)
})
}
}
func LogMiddleware(logger *log.Logger) Adapter {
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r)
path := r.URL.Path
if len(r.URL.RawQuery) > 0 {
path += "?" + r.URL.RawQuery
}
logger.Printf("%s %s [%s]\n", r.Method, path, r.RemoteAddr)
})
}
}

View File

@ -1,23 +0,0 @@
package coquelicot
type option func(*Storage)
func (s *Storage) Option(opts ...option) {
for _, opt := range opts {
opt(s)
}
}
// Verbosity sets verbosity level (1 to 3).
func Verbosity(level int) option {
return func(s *Storage) {
s.verbosity = level
}
}
// Convert generates an image thumbnail using ImageMagick.
func Convert(b bool) option {
return func(s *Storage) {
makeThumbnail = b
}
}

View File

@ -1,129 +0,0 @@
package coquelicot
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
"path/filepath"
"testing"
"code.google.com/p/go-uuid/uuid"
"github.com/stretchr/testify/assert"
)
const dummy = "bin/coquelicot/dummy"
func TestUploadMultipart(t *testing.T) {
assert := assert.New(t)
var body bytes.Buffer
mw := multipart.NewWriter(&body)
if err := writeMPbody(dummy+"/32509211_news_bigpic.jpg", mw); err != nil {
assert.Error(err)
}
if err := writeMPbody(dummy+"/kino.jpg", mw); err != nil {
assert.Error(err)
}
mw.Close()
req, _ := http.NewRequest("POST", "/files", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.AddCookie(&http.Cookie{Name: "coquelicot", Value: "abcdef"})
files, err := process(req, dummy+"/root_storage")
assert.Nil(err)
assert.Equal("kino.jpg", files[1].Filename)
assert.Equal("image", files[1].BaseMime)
}
func TestUploadBinary(t *testing.T) {
assert := assert.New(t)
req, _ := http.NewRequest("POST", "/files", nil)
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("X-File", dummy+"/bin-data")
req.Header.Set("Content-Disposition", `attachment; filename="basta.png"`)
req.AddCookie(&http.Cookie{Name: "coquelicot", Value: "abcdef"})
files, err := process(req, dummy+"/root_storage")
assert.Nil(err)
assert.Equal("basta.png", files[0].Filename)
assert.Equal("image", files[0].BaseMime)
}
func TestUploadChunked(t *testing.T) {
assert := assert.New(t)
storage := dummy + "/root_storage"
fname := dummy + "/kino.jpg"
f, _ := os.Open(fname)
defer f.Close()
cookie := &http.Cookie{Name: "coquelicot", Value: uuid.New()}
req := createChunkRequest(f, 0, 24999)
req.AddCookie(cookie)
files, err := process(req, storage)
assert.Equal(incomplete, err)
assert.Equal(25000, int(files[0].Size))
req = createChunkRequest(f, 25000, 49999)
req.AddCookie(cookie)
files, err = process(req, storage)
assert.Equal(incomplete, err)
assert.Equal(50000, int(files[0].Size))
req = createChunkRequest(f, 50000, 52096)
req.AddCookie(cookie)
files, err = process(req, storage)
assert.Nil(err)
assert.Equal(52097, int(files[0].Size))
assert.Equal("kino.jpg", files[0].Filename)
}
func createChunkRequest(f *os.File, start int64, end int64) *http.Request {
var body bytes.Buffer
mw := multipart.NewWriter(&body)
fi, _ := f.Stat()
fw, _ := mw.CreateFormFile("files[]", fi.Name())
io.CopyN(fw, f, end-start+1)
mw.Close()
req, _ := http.NewRequest("POST", "/files", &body)
req.Header.Set("Content-Type", mw.FormDataContentType())
req.Header.Set("Content-Disposition", `attachment; filename="`+fi.Name()+`"`)
req.Header.Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fi.Size()))
return req
}
func TestTempFileChunks(t *testing.T) {
assert := assert.New(t)
file, err := tempFileChunks(0, dummy+"/root_storage", "abcdef", "kino.jpg")
assert.Nil(err)
assert.NotNil(file)
}
func writeMPbody(fname string, mw *multipart.Writer) error {
fw, _ := mw.CreateFormFile("files[]", filepath.Base(fname))
f, err := os.Open(fname)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(fw, f)
if err != nil {
return err
}
return nil
}

View File

@ -1,157 +0,0 @@
package coquelicot
import (
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"path"
"time"
// "github.com/gin-gonic/gin"
"github.com/pborman/uuid"
)
type H map[string]interface{}
func toJSON(w http.ResponseWriter, code int, obj interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
b, err := json.Marshal(obj)
if err != nil {
log.Println("error:", err.Error())
return
}
if _, err := w.Write(b); err != nil {
log.Println("error:", err.Error())
return
}
}
// ResumeHandler allows resuming a file upload.
//func (s *Storage) ResumeHandler(c *gin.Context) {
func (s *Storage) ResumeHandler(w http.ResponseWriter, r *http.Request) {
status := http.StatusOK
filename := r.URL.Query().Get("file")
cookie, _ := r.Cookie("coquelicot")
offset := int64(0)
if cookie != nil {
hasher := md5.New()
hasher.Write([]byte(cookie.Value + filename))
chunkname := hex.EncodeToString(hasher.Sum(nil))
fi, err := os.Stat(path.Join(s.output, "chunks", chunkname))
if err != nil {
if !os.IsNotExist(err) {
toJSON(w, http.StatusInternalServerError, H{
"status": http.StatusText(http.StatusInternalServerError),
"error": fmt.Sprintf("Resume error: %q", err.Error()),
})
return
}
} else {
offset = fi.Size()
}
}
toJSON(w, status, H{"status": http.StatusText(status), "file": H{"size": offset}})
}
// UploadHandler is the endpoint for uploading and storing files.
//func (s *Storage) UploadHandler(c *gin.Context) {
func (s *Storage) UploadHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == "GET" {
status := http.StatusOK
// FIXME: nil content
toJSON(w, status, H{"status": http.StatusText(status), "files": nil})
return
}
if r.Method != "POST" {
http.NotFound(w, r)
return
}
converts, err := getConvertParams(r)
if err != nil {
toJSON(w, http.StatusBadRequest, H{
"status": "error",
"error": fmt.Sprintf("Query params: %s", err),
})
return
}
converts["original"] = ""
// File upload cookie so we can keep track of chunks.
cookie, _ := r.Cookie("coquelicot")
if cookie == nil {
cookie = &http.Cookie{
Name: "coquelicot",
Value: uuid.New(),
Expires: time.Now().Add(2 * 24 * time.Hour),
Path: "/",
}
r.AddCookie(cookie)
http.SetCookie(w, cookie)
}
// Performs the processing of writing data into chunk files.
files, err := process(r, s.StorageDir())
if err == incomplete {
toJSON(w, http.StatusOK, H{
"status": http.StatusText(http.StatusOK),
"file": H{"size": files[0].Size},
})
return
}
if err != nil {
toJSON(w, http.StatusBadRequest, H{
"status": http.StatusText(http.StatusBadRequest),
"error": fmt.Sprintf("Upload error: %q", err.Error()),
})
return
}
data := make([]map[string]interface{}, 0)
// Expected status if no error
status := http.StatusCreated
for _, ofile := range files {
println("1",ofile.Filename,"2",ofile.Filepath,"3",ofile.Size,"4",ofile.BaseMime)
// true to delete final chunk
attachment, err := create(s.StorageDir(), ofile, converts, true)
if err != nil {
data = append(data, map[string]interface{}{
"name": ofile.Filename,
"size": ofile.Size,
"error": err.Error(),
})
status = http.StatusInternalServerError
continue
}
data = append(data, attachment.ToJson())
}
toJSON(w, status, H{"status": http.StatusText(status), "files": data})
}
// Get parameters for convert from Request query string
func getConvertParams(req *http.Request) (map[string]string, error) {
raw_converts := req.URL.Query().Get("converts")
if raw_converts == "" {
raw_converts = "{}"
}
convert := make(map[string]string)
err := json.Unmarshal([]byte(raw_converts), &convert)
if err != nil {
return nil, err
}
return convert, nil
}