Add preliminary metadata support
This commit is contained in:
parent
674c4be3e3
commit
a10b838f4d
|
@ -28,3 +28,4 @@ _testmain.go
|
||||||
|
|
||||||
linx-server
|
linx-server
|
||||||
files/
|
files/
|
||||||
|
meta/
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get what the unix timestamp will be in "seconds".
|
||||||
|
func getFutureTimestamp(seconds int32) (ts int32) {
|
||||||
|
now := int32(time.Now().Unix())
|
||||||
|
|
||||||
|
if seconds == 0 {
|
||||||
|
ts = 0
|
||||||
|
} else {
|
||||||
|
ts = now + seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if a file with expiry set to "ts" has expired yet
|
||||||
|
func isTsExpired(ts int32) (expired bool) {
|
||||||
|
now := int32(time.Now().Unix())
|
||||||
|
|
||||||
|
if ts == 0 {
|
||||||
|
expired = false
|
||||||
|
} else if now > ts {
|
||||||
|
expired = true
|
||||||
|
} else {
|
||||||
|
expired = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if the given filename is expired
|
||||||
|
func isFileExpired(filename string) (bool, error) {
|
||||||
|
exp, err := metadataGetExpiry(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return true, err
|
||||||
|
} else {
|
||||||
|
return isTsExpired(exp), err
|
||||||
|
}
|
||||||
|
}
|
12
fileserve.go
12
fileserve.go
|
@ -18,7 +18,17 @@ func fileServeHandler(c web.C, w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// plug file expiry checking here
|
expired, expErr := isFileExpired(fileName)
|
||||||
|
|
||||||
|
if expErr != nil {
|
||||||
|
// Error reading metadata, pretend it's expired
|
||||||
|
notFoundHandler(c, w, r)
|
||||||
|
// TODO log error internally
|
||||||
|
return
|
||||||
|
} else if expired {
|
||||||
|
notFoundHandler(c, w, r)
|
||||||
|
// TODO delete the file
|
||||||
|
}
|
||||||
|
|
||||||
http.ServeFile(w, r, filePath)
|
http.ServeFile(w, r, filePath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Write metadata from Upload struct to file
|
||||||
|
func metadataWrite(filename string, upload *Upload) error {
|
||||||
|
// Write metadata, overwriting if necessary
|
||||||
|
|
||||||
|
file, err := os.Create(path.Join(Config.metaDir, upload.Filename))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
w := bufio.NewWriter(file)
|
||||||
|
|
||||||
|
fmt.Fprintln(w, upload.Expiry)
|
||||||
|
fmt.Fprintln(w, upload.DeleteKey)
|
||||||
|
fmt.Fprintln(w, upload.DebugInfo)
|
||||||
|
|
||||||
|
return w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return list of strings from a filename's metadata source
|
||||||
|
func metadataRead(filename string) ([]string, error) {
|
||||||
|
file, err := os.Create(path.Join(Config.metaDir, filename))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var lines []string
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
lines = append(lines, scanner.Text())
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines, scanner.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func metadataGetExpiry(filename string) (int32, error) {
|
||||||
|
metadata, err := metadataRead(filename)
|
||||||
|
|
||||||
|
if len(metadata) < 1 {
|
||||||
|
err := errors.New("ERR: Metadata file does not contain expiry")
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX in this case it's up to the caller to determine proper behavior
|
||||||
|
// for a nonexistant metadata file or broken file
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiry int64
|
||||||
|
expiry, err = strconv.ParseInt(metadata[0], 10, 32)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
} else {
|
||||||
|
return int32(expiry), err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func metadataGetDeleteKey(filename string) (string, error) {
|
||||||
|
metadata, err := metadataRead(filename)
|
||||||
|
|
||||||
|
if len(metadata) < 2 {
|
||||||
|
err := errors.New("ERR: Metadata file does not contain deletion key")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
} else {
|
||||||
|
return metadata[1], err
|
||||||
|
}
|
||||||
|
}
|
30
server.go
30
server.go
|
@ -2,12 +2,12 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"os"
|
"os"
|
||||||
"fmt"
|
"regexp"
|
||||||
|
|
||||||
"github.com/flosch/pongo2"
|
"github.com/flosch/pongo2"
|
||||||
"github.com/zenazn/goji"
|
"github.com/zenazn/goji"
|
||||||
|
@ -17,6 +17,7 @@ import (
|
||||||
var Config struct {
|
var Config struct {
|
||||||
bind string
|
bind string
|
||||||
filesDir string
|
filesDir string
|
||||||
|
metaDir string
|
||||||
noLogs bool
|
noLogs bool
|
||||||
siteName string
|
siteName string
|
||||||
siteURL string
|
siteURL string
|
||||||
|
@ -26,26 +27,41 @@ func main() {
|
||||||
flag.StringVar(&Config.bind, "b", "127.0.0.1:8080",
|
flag.StringVar(&Config.bind, "b", "127.0.0.1:8080",
|
||||||
"host to bind to (default: 127.0.0.1:8080)")
|
"host to bind to (default: 127.0.0.1:8080)")
|
||||||
flag.StringVar(&Config.filesDir, "filespath", "files/",
|
flag.StringVar(&Config.filesDir, "filespath", "files/",
|
||||||
"path to files directory (including trailing slash)")
|
"path to files directory")
|
||||||
|
flag.StringVar(&Config.metaDir, "metapath", "meta/",
|
||||||
|
"path to metadata directory")
|
||||||
flag.BoolVar(&Config.noLogs, "nologs", false,
|
flag.BoolVar(&Config.noLogs, "nologs", false,
|
||||||
"remove stdout output for each request")
|
"remove stdout output for each request")
|
||||||
flag.StringVar(&Config.siteName, "sitename", "linx",
|
flag.StringVar(&Config.siteName, "sitename", "linx",
|
||||||
"name of the site")
|
"name of the site")
|
||||||
flag.StringVar(&Config.siteURL, "siteurl", "http://"+Config.bind+"/",
|
flag.StringVar(&Config.siteURL, "siteurl", "http://"+Config.bind+"/",
|
||||||
"site base url (including trailing slash)")
|
"site base url")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if Config.noLogs {
|
if Config.noLogs {
|
||||||
goji.Abandon(middleware.Logger)
|
goji.Abandon(middleware.Logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
// make directory if needed
|
// make directories if needed
|
||||||
err := os.MkdirAll(Config.filesDir, 0755)
|
var err error
|
||||||
|
|
||||||
|
err = os.MkdirAll(Config.filesDir, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error: could not create files directory")
|
fmt.Printf("Error: could not create files directory\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = os.MkdirAll(Config.metaDir, 0700)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error: could not create metadata directory\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure siteURL ends wth '/'
|
||||||
|
if lastChar := Config.siteURL[len(Config.siteURL)-1:]; lastChar != "/" {
|
||||||
|
Config.siteURL = Config.siteURL + "/"
|
||||||
|
}
|
||||||
|
|
||||||
// Template Globals
|
// Template Globals
|
||||||
pongo2.DefaultSet.Globals["sitename"] = Config.siteName
|
pongo2.DefaultSet.Globals["sitename"] = Config.siteName
|
||||||
|
|
||||||
|
|
56
upload.go
56
upload.go
|
@ -15,21 +15,55 @@ import (
|
||||||
"github.com/zenazn/goji/web"
|
"github.com/zenazn/goji/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Describes metadata directly from the user request
|
||||||
type UploadRequest struct {
|
type UploadRequest struct {
|
||||||
src io.Reader
|
src io.Reader
|
||||||
filename string
|
filename string
|
||||||
expiry int
|
expiry int32 // Seconds until expiry, 0 = never
|
||||||
randomBarename bool
|
randomBarename bool
|
||||||
|
deletionKey string // Empty string if not defined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metadata associated with a file as it would actually be stored
|
||||||
type Upload struct {
|
type Upload struct {
|
||||||
Filename string
|
Filename string // Final filename on disk
|
||||||
Size int64
|
Size int64
|
||||||
Expiry int
|
Expiry int32 // Unix timestamp of expiry, 0=never
|
||||||
|
DeleteKey string // Deletion key, one generated if not provided
|
||||||
|
DebugInfo string // Optional field to store whatever
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadHeaderProcess(r *http.Request, upReq *UploadRequest) {
|
||||||
|
// For legacy reasons
|
||||||
|
upReq.randomBarename = false
|
||||||
|
if r.Header.Get("X-Randomized-Filename") == "yes" {
|
||||||
|
upReq.randomBarename = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Header.Get("X-Randomized-Barename") == "yes" {
|
||||||
|
upReq.randomBarename = true
|
||||||
|
}
|
||||||
|
|
||||||
|
upReq.deletionKey = r.Header.Get("X-Delete-Key")
|
||||||
|
|
||||||
|
// Get seconds until expiry. Non-integer responses never expire.
|
||||||
|
expStr := r.Header.Get("X-File-Expiry")
|
||||||
|
if expStr == "" {
|
||||||
|
upReq.expiry = 0
|
||||||
|
} else {
|
||||||
|
expiry, err := strconv.ParseInt(expStr, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
upReq.expiry = 0
|
||||||
|
} else {
|
||||||
|
upReq.expiry = int32(expiry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func uploadPostHandler(c web.C, w http.ResponseWriter, r *http.Request) {
|
func uploadPostHandler(c web.C, w http.ResponseWriter, r *http.Request) {
|
||||||
upReq := UploadRequest{}
|
upReq := UploadRequest{}
|
||||||
|
uploadHeaderProcess(r, &upReq)
|
||||||
|
|
||||||
if r.Header.Get("Content-Type") == "application/octet-stream" {
|
if r.Header.Get("Content-Type") == "application/octet-stream" {
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
@ -71,6 +105,7 @@ func uploadPostHandler(c web.C, w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
func uploadPutHandler(c web.C, w http.ResponseWriter, r *http.Request) {
|
func uploadPutHandler(c web.C, w http.ResponseWriter, r *http.Request) {
|
||||||
upReq := UploadRequest{}
|
upReq := UploadRequest{}
|
||||||
|
uploadHeaderProcess(r, &upReq)
|
||||||
|
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
upReq.filename = c.URLParams["name"]
|
upReq.filename = c.URLParams["name"]
|
||||||
|
@ -86,6 +121,7 @@ func uploadPutHandler(c web.C, w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func processUpload(upReq UploadRequest) (upload Upload, err error) {
|
func processUpload(upReq UploadRequest) (upload Upload, err error) {
|
||||||
|
// Determine the appropriate filename, then write to disk
|
||||||
barename, extension := barePlusExt(upReq.filename)
|
barename, extension := barePlusExt(upReq.filename)
|
||||||
|
|
||||||
if upReq.randomBarename || len(barename) == 0 {
|
if upReq.randomBarename || len(barename) == 0 {
|
||||||
|
@ -120,6 +156,18 @@ func processUpload(upReq UploadRequest) (upload Upload, err error) {
|
||||||
}
|
}
|
||||||
defer dst.Close()
|
defer dst.Close()
|
||||||
|
|
||||||
|
// Get the rest of the metadata needed for storage
|
||||||
|
upload.Expiry = getFutureTimestamp(upReq.expiry)
|
||||||
|
|
||||||
|
// If no delete key specified, pick a random one.
|
||||||
|
if upReq.deletionKey == "" {
|
||||||
|
upload.DeleteKey = uuid.New()[:30]
|
||||||
|
} else {
|
||||||
|
upload.DeleteKey = upReq.deletionKey
|
||||||
|
}
|
||||||
|
|
||||||
|
metadataWrite(upload.Filename, &upload)
|
||||||
|
|
||||||
bytes, err := io.Copy(dst, upReq.src)
|
bytes, err := io.Copy(dst, upReq.src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
Loading…
Reference in New Issue