rushlink/fileupload.go

195 lines
4.4 KiB
Go
Raw Normal View History

2019-11-10 19:03:57 +01:00
package rushlink
import (
"encoding/hex"
"hash/crc32"
"io"
"log"
"net/http"
"net/url"
"os"
"path"
"github.com/google/uuid"
"github.com/pkg/errors"
bolt "go.etcd.io/bbolt"
)
// Use the Castagnoli checksum because of the acceleration on Intel CPUs
var checksumTable = crc32.MakeTable(crc32.Castagnoli)
// Where to store the uploaded files
var fileStoreDir = ""
// Custom HTTP filesystem handler
type fileUploadFileSystem struct {
fs http.FileSystem
}
// Open opens file
func (fs fileUploadFileSystem) Open(path string) (http.File, error) {
log.Println(path)
file, err := fs.fs.Open(path)
if err != nil {
return nil, errors.Wrap(err, "opening file")
}
stat, err := file.Stat()
if err != nil {
return nil, errors.Wrap(err, "file.Stat()")
}
if stat.IsDir() {
return nil, errors.New("directory index not allowed")
}
return file, nil
}
type fileUploadState int
type fileUpload struct {
State fileUploadState
ID uuid.UUID
FileName string
ContentType string
Checksum uint32
}
const (
dirMode os.FileMode = 0750
fileMode os.FileMode = 0640
)
const (
fileUploadStateUndef fileUploadState = 0
fileUploadStatePresent = 1
fileUploadStateDeleted = 2
)
func (t fileUploadState) String() string {
switch t {
case fileUploadStateUndef:
return "unknown"
case fileUploadStatePresent:
return "present"
case fileUploadStateDeleted:
return "deleted"
default:
return "invalid"
}
}
func OpenFileStore(path string) error {
if path == "" {
2019-11-22 18:41:54 +01:00
return errors.New("file-store not set")
2019-11-10 19:03:57 +01:00
}
// Try to create the file store directory if it does not yet exist
if err := os.MkdirAll(path, dirMode); err != nil {
return errors.Wrap(err, "creating file store directory")
}
fileStoreDir = path[:]
return nil
}
func newFileUpload(tx *bolt.Tx, r io.Reader, fileName string, contentType string) (*fileUpload, error) {
id, err := uuid.NewRandom()
if err != nil {
return nil, errors.Wrap(err, "generating UUID")
}
filePath := fileStorePath(id, fileName)
if err := os.Mkdir(path.Dir(filePath), dirMode); err != nil {
return nil, errors.Wrap(err, "creating file dir")
}
file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, fileMode)
if err != nil {
return nil, errors.Wrap(err, "opening file")
}
defer file.Close()
hash := crc32.New(checksumTable)
tee := io.TeeReader(r, hash)
_, err = io.Copy(file, tee)
if err != nil {
return nil, errors.Wrap(err, "writing to file")
}
fu := &fileUpload{
State: fileUploadStatePresent,
ID: id,
FileName: fileName,
ContentType: contentType,
Checksum: hash.Sum32(),
}
if err := fu.save(tx); err != nil {
return nil, err
}
return fu, nil
}
func getFileUpload(tx *bolt.Tx, id uuid.UUID) (*fileUpload, error) {
bucket := tx.Bucket([]byte(BUCKET_FILE_UPLOAD))
if bucket == nil {
return nil, errors.Errorf("bucket %v does not exist", BUCKET_FILE_UPLOAD)
}
storedBytes := bucket.Get(id[:])
if storedBytes == nil {
return nil, nil
}
fu := &fileUpload{}
err := Unmarshal(storedBytes, fu)
return fu, err
}
func (fu *fileUpload) save(tx *bolt.Tx) error {
bucket := tx.Bucket([]byte(BUCKET_FILE_UPLOAD))
if bucket == nil {
return errors.Errorf("bucket %v does not exist", BUCKET_FILE_UPLOAD)
}
buf, err := Marshal(fu)
if err != nil {
return errors.Wrap(err, "encoding for database failed")
}
if err := bucket.Put(fu.ID[:], buf); err != nil {
return errors.Wrap(err, "database transaction failed")
}
return nil
}
func (fu *fileUpload) delete(tx *bolt.Tx) error {
2019-11-22 18:41:54 +01:00
// Remove the file in the backend
filePath := fileStorePath(fu.ID, fu.FileName)
if err := os.Remove(filePath); err != nil {
return err
}
// Update the file in the server
if err := (&fileUpload{
2019-11-10 19:03:57 +01:00
ID: fu.ID,
State: fileUploadStateDeleted,
2019-11-22 18:41:54 +01:00
}).save(tx); err != nil {
return err
}
// Cleanup the parent directory
wrap := "deletion succeeded, but removing the file directory has failed"
return errors.Wrap(os.Remove(path.Dir(filePath)), wrap)
2019-11-10 19:03:57 +01:00
}
func (fu *fileUpload) url() *url.URL {
rawurl := "/uploads/" + hex.EncodeToString(fu.ID[:]) + "/" + fu.FileName
urlParse, err := url.Parse(rawurl)
if err != nil {
panic("could not construct /uploads/ url")
}
return urlParse
}
func fileStorePath(id uuid.UUID, fileName string) string {
if fileStoreDir == "" {
panic("fileStoreDir called while the file store path has not been set")
}
return path.Join(fileStoreDir, hex.EncodeToString(id[:]), fileName)
}