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 == "" { return errors.New("file store path not set") } // 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 { // Replace the old paste with a new empty paste return (&fileUpload{ ID: fu.ID, State: fileUploadStateDeleted, }).save(tx) } 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) }