Implemented uploading for Backblaze & Disk (v1)

Implemented file uploading for both the disk and Backblaze providers.
Also implements a UI element and frontend logic for doing so from the
frontend.

Disk will write directly to disk, while the Backblaze provider will
attempt to stream the file from memory directly to the POST body. This
could introduce some problems down the line, so caching to disk then
uploading in the background may need to be implemented. It also
performs the final upload using a goroutine so the end client can
continue on it's merry way.

Backblaze proved a bit tricky to do, and considering switching to using
a library for managing Backblaze going forward.
This commit is contained in:
Gabriel Simmer 2020-03-30 20:51:26 +01:00
parent 9cd7ab75e8
commit d86ed1233f
No known key found for this signature in database
GPG key ID: 33BA4D83B160A0A9
7 changed files with 145 additions and 8 deletions

View file

@ -75,3 +75,25 @@ body {
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
} }
} }
input[type="file"] {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}
input[type="file"] + label {
font-size: 1.25em;
font-weight: 700;
color: white;
background-color: black;
display: inline-block;
}
input[type="file"]:focus + label,
input[type="file"] + label:hover {
background-color: red;
}

View file

@ -1,6 +1,7 @@
// Register our router, and fire it off initially in case user is being linked a dir. // Register our router, and fire it off initially in case user is being linked a dir.
window.addEventListener("hashchange", router, false); window.addEventListener("hashchange", router, false);
router() router()
let input = ""
function getFileListing(provider, path = "") { function getFileListing(provider, path = "") {
fetch(`/api/files/${provider}${path}`) fetch(`/api/files/${provider}${path}`)
@ -10,6 +11,9 @@ function getFileListing(provider, path = "") {
.then((data) => { .then((data) => {
let files = data["Files"] let files = data["Files"]
html` html`
<form action="#" method="post">
<input type="file" id="file" data-dir="${provider}${path}"><label for="file">Upload</label>
</form>
<div class="grid-sm"> <div class="grid-sm">
${files.map(file => ${files.map(file =>
`<a href="${!file.IsDirectory ? `/api/files/${provider}${path}/${file.Name}` : `#${provider}/${path !== "" ? path.replace("/","") + "/" : ""}${file.Name}`}"> `<a href="${!file.IsDirectory ? `/api/files/${provider}${path}/${file.Name}` : `#${provider}/${path !== "" ? path.replace("/","") + "/" : ""}${file.Name}`}">
@ -19,6 +23,9 @@ function getFileListing(provider, path = "") {
).join('')} ).join('')}
</div> </div>
` `
input = document.getElementById("file")
input.addEventListener("change", onSelectFile, false)
}) })
} }
@ -56,6 +63,20 @@ function router(event = null) {
getFileListing(provider, "/" + path) getFileListing(provider, "/" + path)
} }
function onSelectFile() {
upload(input.getAttribute("data-dir"), input.files[0])
}
function upload(path, file) {
let formData = new FormData()
formData.append("file", file)
fetch(`/api/files/${path}`, {
method: "POST",
body: formData
}).then(response => response.text())
.then(text => console.log(text))
.then(router())
}
// Tagged template function for parsing a string of text as HTML objects // Tagged template function for parsing a string of text as HTML objects
// <3 @innovati for this brilliance. // <3 @innovati for this brilliance.
function html(strings, ...things) { function html(strings, ...things) {

View file

@ -2,10 +2,12 @@ package files
import ( import (
"bytes" "bytes"
"crypto/sha1"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"mime/multipart"
"net/http" "net/http"
"strings" "strings"
) )
@ -44,6 +46,11 @@ type BackblazeBucketInfoPayload struct {
Buckets []BackblazeBucketInfo `json:"buckets"` Buckets []BackblazeBucketInfo `json:"buckets"`
} }
type BackblazeUploadInfo struct {
UploadUrl string `json:"uploadUrl"`
AuthToken string `json:"authorizationToken"`
}
// Call Backblaze API endpoint to authorize and gather facts. // Call Backblaze API endpoint to authorize and gather facts.
func (bp *BackblazeProvider) Setup(args map[string]string) bool { func (bp *BackblazeProvider) Setup(args map[string]string) bool {
applicationKeyId := args["applicationKeyId"] applicationKeyId := args["applicationKeyId"]
@ -183,7 +190,66 @@ func (bp *BackblazeProvider) ViewFile(path string, w io.Writer) {
} }
} }
func (bp *BackblazeProvider) SaveFile(contents []byte, path string) bool { func (bp *BackblazeProvider) SaveFile(file multipart.File, handler *multipart.FileHeader, path string) bool {
client := &http.Client{}
bucketIdPayload := fmt.Sprintf(`{"bucketId": "%s"}`, bp.Bucket)
req, err := http.NewRequest("POST", bp.Location + "/b2api/v2/b2_get_upload_url",
bytes.NewBuffer([]byte(bucketIdPayload)))
if err != nil {
fmt.Println(err.Error())
return false
}
req.Header.Add("Authorization", bp.Authentication)
res, err := client.Do(req)
if err != nil {
fmt.Println(err.Error())
return false
}
bucketData, err := ioutil.ReadAll(res.Body)
if err != nil {
fmt.Println(err.Error())
return false
}
var data BackblazeUploadInfo
json.Unmarshal(bucketData, &data)
req, err = http.NewRequest("POST",
data.UploadUrl,
file,
)
if err != nil {
fmt.Println(err.Error())
return false
}
// Read the content
var bodyBytes []byte
if req.Body != nil {
bodyBytes, _ = ioutil.ReadAll(req.Body)
}
req.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
// Calculate SHA1 and add required headers.
fileSha := sha1.New()
fileSha.Write(bodyBytes)
req.Header.Add("Authorization", data.AuthToken)
req.Header.Add("X-Bz-File-Name", handler.Filename)
req.Header.Add("Content-Type", "b2/x-auto")
req.Header.Add("X-Bz-Content-Sha1", fmt.Sprintf("%x", fileSha.Sum(nil)))
req.ContentLength = handler.Size
// Upload in background.
go func() {
res, err = client.Do(req)
if err != nil {
fmt.Println(err.Error())
}
}()
return true return true
} }

View file

@ -1,8 +1,10 @@
package files package files
import ( import (
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"mime/multipart"
"os" "os"
"strings" "strings"
) )
@ -53,11 +55,15 @@ func (dp *DiskProvider) ViewFile(path string, w io.Writer) {
} }
} }
func (dp *DiskProvider) SaveFile(contents []byte, path string) bool { func (dp *DiskProvider) SaveFile(file multipart.File, handler *multipart.FileHeader, path string) bool {
err := ioutil.WriteFile(path, contents, 0600) f, err := os.OpenFile(dp.Location + path + "/" + handler.Filename, os.O_WRONLY|os.O_CREATE, 0666)
if err != nil { if err != nil {
fmt.Println(err.Error())
return false return false
} }
defer f.Close()
io.Copy(f, file)
return true return true
} }
@ -72,4 +78,4 @@ func (dp *DiskProvider) DetermineType(path string) string {
} }
return "file" return "file"
} }

View file

@ -2,6 +2,7 @@ package files
import ( import (
"io" "io"
"mime/multipart"
) )
type FileProvider struct { type FileProvider struct {
@ -32,7 +33,7 @@ type FileProviderInterface interface {
Setup(args map[string]string) bool Setup(args map[string]string) bool
GetDirectory(path string) Directory GetDirectory(path string) Directory
ViewFile(path string, w io.Writer) ViewFile(path string, w io.Writer)
SaveFile(contents []byte, path string) bool SaveFile(file multipart.File, handler *multipart.FileHeader, path string) bool
DetermineType(path string) string DetermineType(path string) string
} }
@ -49,7 +50,7 @@ func (f FileProvider) ViewFile(path string, w io.Writer) {
return return
} }
func (f FileProvider) SaveFile(contents []byte, path string) bool { func (f FileProvider) SaveFile(file multipart.File, handler *multipart.FileHeader, path string) bool {
return false return false
} }

View file

@ -2,6 +2,7 @@ package router
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/gmemstr/nas/common" "github.com/gmemstr/nas/common"
"github.com/gmemstr/nas/files" "github.com/gmemstr/nas/files"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -39,6 +40,26 @@ func HandleProvider() common.Handler {
} }
w.Write(data) w.Write(data)
} }
if r.Method == "POST" {
providerCodename := vars["provider"]
providerCodename = strings.Replace(providerCodename, "/", "", -1)
provider := *files.Providers[providerCodename]
err := r.ParseMultipartForm(32 << 20)
if err != nil {
w.Write([]byte("unable to parse form"))
fmt.Println(err.Error())
return nil
}
file, handler, err := r.FormFile("file")
defer file.Close()
success := provider.SaveFile(file, handler, vars["file"])
if !success {
w.Write([]byte("unable to save file"))
return nil
}
w.Write([]byte("saved file"))
}
return nil return nil
} }

View file

@ -56,12 +56,12 @@ func Init() *mux.Router {
r.Handle(`/api/files/{provider:[a-zA-Z0-9]+\/*}`, Handle( r.Handle(`/api/files/{provider:[a-zA-Z0-9]+\/*}`, Handle(
//auth.RequireAuthorization(1), //auth.RequireAuthorization(1),
HandleProvider(), HandleProvider(),
)).Methods("GET") )).Methods("GET", "POST")
r.Handle(`/api/files/{provider}/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle( r.Handle(`/api/files/{provider}/{file:[a-zA-Z0-9=\-\/\s.,&_+]+}`, Handle(
//auth.RequireAuthorization(1), //auth.RequireAuthorization(1),
HandleProvider(), HandleProvider(),
)).Methods("GET") )).Methods("GET", "POST")
return r return r
} }