mirror of
https://gitee.com/rainbond/Rainbond.git
synced 2024-11-30 02:38:17 +08:00
[REV] Optimize the upload file process
This commit is contained in:
parent
24b1a2b5cc
commit
789609e67f
@ -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
|
||||
|
4
api/controller/coquelicot.v1/.gitignore
vendored
4
api/controller/coquelicot.v1/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
*.swp
|
||||
tags
|
||||
bin/coquelicot/coquelicot
|
||||
bin/coquelicot/dummy/root_storage/*
|
@ -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
|
||||
```
|
@ -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.
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
@ -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",
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
|
||||
Build it with `./build.sh` which includes a version number (from git).
|
@ -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
|
@ -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 |
@ -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))
|
||||
}
|
@ -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"])
|
||||
}
|
@ -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
|
@ -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}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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())
|
||||
}
|
@ -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")
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
@ -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 = ""
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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 {
|
@ -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 == "" {
|
@ -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
|
||||
}
|
||||
|
48
api/controller/upload/web.go
Normal file
48
api/controller/upload/web.go
Normal 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)
|
||||
}
|
4
vendor/gopkg.in/gotsunami/coquelicot.v1/.gitignore
generated
vendored
4
vendor/gopkg.in/gotsunami/coquelicot.v1/.gitignore
generated
vendored
@ -1,4 +0,0 @@
|
||||
*.swp
|
||||
tags
|
||||
bin/coquelicot/coquelicot
|
||||
bin/coquelicot/dummy/root_storage/*
|
142
vendor/gopkg.in/gotsunami/coquelicot.v1/Draft.md
generated
vendored
142
vendor/gopkg.in/gotsunami/coquelicot.v1/Draft.md
generated
vendored
@ -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
|
||||
```
|
22
vendor/gopkg.in/gotsunami/coquelicot.v1/LICENSE
generated
vendored
22
vendor/gopkg.in/gotsunami/coquelicot.v1/LICENSE
generated
vendored
@ -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.
|
||||
|
73
vendor/gopkg.in/gotsunami/coquelicot.v1/README.md
generated
vendored
73
vendor/gopkg.in/gotsunami/coquelicot.v1/README.md
generated
vendored
@ -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);
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
81
vendor/gopkg.in/gotsunami/coquelicot.v1/attachment.go
generated
vendored
81
vendor/gopkg.in/gotsunami/coquelicot.v1/attachment.go
generated
vendored
@ -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
|
||||
}
|
39
vendor/gopkg.in/gotsunami/coquelicot.v1/attachment_test.go
generated
vendored
39
vendor/gopkg.in/gotsunami/coquelicot.v1/attachment_test.go
generated
vendored
@ -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",
|
||||
}
|
||||
}
|
2
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/README.md
generated
vendored
2
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/README.md
generated
vendored
@ -1,2 +0,0 @@
|
||||
|
||||
Build it with `./build.sh` which includes a version number (from git).
|
17
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/build.sh
generated
vendored
17
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/build.sh
generated
vendored
@ -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
|
13
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/config.go
generated
vendored
13
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/config.go
generated
vendored
@ -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")
|
||||
)
|
BIN
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/dummy/32509211_news_bigpic.jpg
generated
vendored
BIN
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/dummy/32509211_news_bigpic.jpg
generated
vendored
Binary file not shown.
Before Width: | Height: | Size: 60 KiB |
BIN
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/dummy/bin-data
generated
vendored
BIN
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/dummy/bin-data
generated
vendored
Binary file not shown.
Before Width: | Height: | Size: 7.0 KiB |
BIN
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/dummy/kino.jpg
generated
vendored
BIN
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/dummy/kino.jpg
generated
vendored
Binary file not shown.
Before Width: | Height: | Size: 51 KiB |
39
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/main.go
generated
vendored
39
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/main.go
generated
vendored
@ -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))
|
||||
}
|
43
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/main_test.go
generated
vendored
43
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/main_test.go
generated
vendored
@ -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"])
|
||||
}
|
45
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/release.sh
generated
vendored
45
vendor/gopkg.in/gotsunami/coquelicot.v1/bin/coquelicot/release.sh
generated
vendored
@ -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
|
39
vendor/gopkg.in/gotsunami/coquelicot.v1/body.go
generated
vendored
39
vendor/gopkg.in/gotsunami/coquelicot.v1/body.go
generated
vendored
@ -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
|
||||
}
|
18
vendor/gopkg.in/gotsunami/coquelicot.v1/coquelicot.go
generated
vendored
18
vendor/gopkg.in/gotsunami/coquelicot.v1/coquelicot.go
generated
vendored
@ -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}
|
||||
}
|
76
vendor/gopkg.in/gotsunami/coquelicot.v1/dir.go
generated
vendored
76
vendor/gopkg.in/gotsunami/coquelicot.v1/dir.go
generated
vendored
@ -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)
|
||||
}
|
19
vendor/gopkg.in/gotsunami/coquelicot.v1/dir_test.go
generated
vendored
19
vendor/gopkg.in/gotsunami/coquelicot.v1/dir_test.go
generated
vendored
@ -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)
|
||||
}
|
40
vendor/gopkg.in/gotsunami/coquelicot.v1/file_default_manager.go
generated
vendored
40
vendor/gopkg.in/gotsunami/coquelicot.v1/file_default_manager.go
generated
vendored
@ -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
|
||||
}
|
75
vendor/gopkg.in/gotsunami/coquelicot.v1/file_image_manager.go
generated
vendored
75
vendor/gopkg.in/gotsunami/coquelicot.v1/file_image_manager.go
generated
vendored
@ -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
|
||||
}
|
113
vendor/gopkg.in/gotsunami/coquelicot.v1/meta.go
generated
vendored
113
vendor/gopkg.in/gotsunami/coquelicot.v1/meta.go
generated
vendored
@ -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
|
||||
}
|
32
vendor/gopkg.in/gotsunami/coquelicot.v1/meta_test.go
generated
vendored
32
vendor/gopkg.in/gotsunami/coquelicot.v1/meta_test.go
generated
vendored
@ -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")
|
||||
}
|
50
vendor/gopkg.in/gotsunami/coquelicot.v1/middleware.go
generated
vendored
50
vendor/gopkg.in/gotsunami/coquelicot.v1/middleware.go
generated
vendored
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
23
vendor/gopkg.in/gotsunami/coquelicot.v1/options.go
generated
vendored
23
vendor/gopkg.in/gotsunami/coquelicot.v1/options.go
generated
vendored
@ -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
|
||||
}
|
||||
}
|
129
vendor/gopkg.in/gotsunami/coquelicot.v1/upload_test.go
generated
vendored
129
vendor/gopkg.in/gotsunami/coquelicot.v1/upload_test.go
generated
vendored
@ -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
|
||||
}
|
157
vendor/gopkg.in/gotsunami/coquelicot.v1/web.go
generated
vendored
157
vendor/gopkg.in/gotsunami/coquelicot.v1/web.go
generated
vendored
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user