Merge pull request #124 from mutantmonkey/cleanup_tool

Add linx-cleanup tool
This commit is contained in:
Andrei Marcu 2017-05-04 23:06:26 -07:00 committed by GitHub
commit ceea32de6b
17 changed files with 235 additions and 98 deletions

1
.gitignore vendored
View File

@ -29,3 +29,4 @@ _testmain.go
linx-server linx-server
files/ files/
meta/ meta/
linx-cleanup

View File

@ -1,7 +1,7 @@
FROM golang:alpine FROM golang:alpine
RUN set -ex \ RUN set -ex \
&& apk add --no-cache --virtual .build-deps git mercurial \ && apk add --no-cache --virtual .build-deps git \
&& go get github.com/andreimarcu/linx-server \ && go get github.com/andreimarcu/linx-server \
&& apk del .build-deps && apk del .build-deps

View File

@ -71,6 +71,23 @@ allowhotlink = true
A helper utility ```linx-genkey``` is provided which hashes keys to the format required in the auth files. A helper utility ```linx-genkey``` is provided which hashes keys to the format required in the auth files.
Cleaning up expired files
-------------------------
When files expire, access is disabled immediately, but the files and metadata
will persist on disk until someone attempts to access them. If you'd like to
automatically clean up files that have expired, you can use the included
`linx-cleanup` utility. To run it automatically, use a cronjob or similar type
of scheduled task.
You should be careful to ensure that only one instance of `linx-client` runs at
a time to avoid unexpected behavior. It does not implement any type of locking.
#### Options
- ```-filespath files/``` -- Path to stored uploads (default is files/)
- ```-metapath meta/``` -- Path to stored information about uploads (default is meta/)
- ```-nologs``` -- (optionally) disable deletion logs in stdout
Deployment Deployment
---------- ----------
Linx-server supports being deployed in a subdirectory (ie. example.com/mylinx/) as well as on its own (example.com/). Linx-server supports being deployed in a subdirectory (ie. example.com/mylinx/) as well as on its own (example.com/).

View File

@ -65,6 +65,21 @@ func (b LocalfsBackend) Size(key string) (int64, error) {
return fileInfo.Size(), nil return fileInfo.Size(), nil
} }
func (b LocalfsBackend) List() ([]string, error) {
var output []string
files, err := ioutil.ReadDir(b.basePath)
if err != nil {
return nil, err
}
for _, file := range files {
output = append(output, file.Name())
}
return output, nil
}
func NewLocalfsBackend(basePath string) LocalfsBackend { func NewLocalfsBackend(basePath string) LocalfsBackend {
return LocalfsBackend{basePath: basePath} return LocalfsBackend{basePath: basePath}
} }

23
backends/meta.go Normal file
View File

@ -0,0 +1,23 @@
package backends
import (
"errors"
"time"
)
type MetaBackend interface {
Get(key string) (Metadata, error)
Put(key string, metadata *Metadata) error
}
type Metadata struct {
DeleteKey string
Sha256sum string
Mimetype string
Size int64
Expiry time.Time
ArchiveFiles []string
ShortURL string
}
var BadMetadata = errors.New("Corrupted metadata.")

View File

@ -0,0 +1,73 @@
package metajson
import (
"bytes"
"encoding/json"
"time"
"github.com/andreimarcu/linx-server/backends"
)
type MetadataJSON struct {
DeleteKey string `json:"delete_key"`
Sha256sum string `json:"sha256sum"`
Mimetype string `json:"mimetype"`
Size int64 `json:"size"`
Expiry int64 `json:"expiry"`
ArchiveFiles []string `json:"archive_files,omitempty"`
ShortURL string `json:"short_url"`
}
type MetaJSONBackend struct {
storage backends.MetaStorageBackend
}
func (m MetaJSONBackend) Put(key string, metadata *backends.Metadata) error {
mjson := MetadataJSON{}
mjson.DeleteKey = metadata.DeleteKey
mjson.Mimetype = metadata.Mimetype
mjson.ArchiveFiles = metadata.ArchiveFiles
mjson.Sha256sum = metadata.Sha256sum
mjson.Expiry = metadata.Expiry.Unix()
mjson.Size = metadata.Size
mjson.ShortURL = metadata.ShortURL
byt, err := json.Marshal(mjson)
if err != nil {
return err
}
if _, err := m.storage.Put(key, bytes.NewBuffer(byt)); err != nil {
return err
}
return nil
}
func (m MetaJSONBackend) Get(key string) (metadata backends.Metadata, err error) {
b, err := m.storage.Get(key)
if err != nil {
return metadata, backends.BadMetadata
}
mjson := MetadataJSON{}
err = json.Unmarshal(b, &mjson)
if err != nil {
return metadata, backends.BadMetadata
}
metadata.DeleteKey = mjson.DeleteKey
metadata.Mimetype = mjson.Mimetype
metadata.ArchiveFiles = mjson.ArchiveFiles
metadata.Sha256sum = mjson.Sha256sum
metadata.Expiry = time.Unix(mjson.Expiry, 0)
metadata.Size = mjson.Size
metadata.ShortURL = mjson.ShortURL
return
}
func NewMetaJSONBackend(storage backends.MetaStorageBackend) MetaJSONBackend {
return MetaJSONBackend{storage: storage}
}

View File

@ -21,3 +21,8 @@ type StorageBackend interface {
ServeFile(key string, w http.ResponseWriter, r *http.Request) ServeFile(key string, w http.ResponseWriter, r *http.Request)
Size(key string) (int64, error) Size(key string) (int64, error)
} }
type MetaStorageBackend interface {
StorageBackend
List() ([]string, error)
}

View File

@ -28,7 +28,7 @@ func deleteHandler(c web.C, w http.ResponseWriter, r *http.Request) {
if metadata.DeleteKey == requestKey { if metadata.DeleteKey == requestKey {
fileDelErr := fileBackend.Delete(filename) fileDelErr := fileBackend.Delete(filename)
metaDelErr := metaBackend.Delete(filename) metaDelErr := metaStorageBackend.Delete(filename)
if (fileDelErr != nil) || (metaDelErr != nil) { if (fileDelErr != nil) || (metaDelErr != nil) {
oopsHandler(c, w, r, RespPLAIN, "Could not delete") oopsHandler(c, w, r, RespPLAIN, "Could not delete")

View File

@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/andreimarcu/linx-server/expiry"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
"github.com/flosch/pongo2" "github.com/flosch/pongo2"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
@ -32,7 +33,7 @@ func fileDisplayHandler(c web.C, w http.ResponseWriter, r *http.Request) {
return return
} }
var expiryHuman string var expiryHuman string
if metadata.Expiry != neverExpire { if metadata.Expiry != expiry.NeverExpire {
expiryHuman = humanize.RelTime(time.Now(), metadata.Expiry, "", "") expiryHuman = humanize.RelTime(time.Now(), metadata.Expiry, "", "")
} }
sizeHuman := humanize.Bytes(uint64(metadata.Size)) sizeHuman := humanize.Bytes(uint64(metadata.Size))

View File

@ -3,6 +3,7 @@ package main
import ( import (
"time" "time"
"github.com/andreimarcu/linx-server/expiry"
"github.com/dustin/go-humanize" "github.com/dustin/go-humanize"
) )
@ -21,14 +22,6 @@ type ExpirationTime struct {
Human string Human string
} }
var neverExpire = time.Unix(0, 0)
// Determine if a file with expiry set to "ts" has expired yet
func isTsExpired(ts time.Time) bool {
now := time.Now()
return ts != neverExpire && now.After(ts)
}
// Determine if the given filename is expired // Determine if the given filename is expired
func isFileExpired(filename string) (bool, error) { func isFileExpired(filename string) (bool, error) {
metadata, err := metadataRead(filename) metadata, err := metadataRead(filename)
@ -36,7 +29,7 @@ func isFileExpired(filename string) (bool, error) {
return false, err return false, err
} }
return isTsExpired(metadata.Expiry), nil return expiry.IsTsExpired(metadata.Expiry), nil
} }
// Return a list of expiration times and their humanized versions // Return a list of expiration times and their humanized versions
@ -45,16 +38,16 @@ func listExpirationTimes() []ExpirationTime {
actualExpiryInList := false actualExpiryInList := false
var expiryList []ExpirationTime var expiryList []ExpirationTime
for _, expiry := range defaultExpiryList { for _, expiryEntry := range defaultExpiryList {
if Config.maxExpiry == 0 || expiry <= Config.maxExpiry { if Config.maxExpiry == 0 || expiryEntry <= Config.maxExpiry {
if expiry == Config.maxExpiry { if expiryEntry == Config.maxExpiry {
actualExpiryInList = true actualExpiryInList = true
} }
duration := time.Duration(expiry) * time.Second duration := time.Duration(expiryEntry) * time.Second
expiryList = append(expiryList, ExpirationTime{ expiryList = append(expiryList, ExpirationTime{
expiry, Seconds: expiryEntry,
humanize.RelTime(epoch, epoch.Add(duration), "", ""), Human: humanize.RelTime(epoch, epoch.Add(duration), "", ""),
}) })
} }
} }

13
expiry/expiry.go Normal file
View File

@ -0,0 +1,13 @@
package expiry
import (
"time"
)
var NeverExpire = time.Unix(0, 0)
// Determine if a file with expiry set to "ts" has expired yet
func IsTsExpired(ts time.Time) bool {
now := time.Now()
return ts != NeverExpire && now.After(ts)
}

View File

@ -5,6 +5,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/andreimarcu/linx-server/backends"
"github.com/zenazn/goji/web" "github.com/zenazn/goji/web"
) )
@ -15,7 +16,7 @@ func fileServeHandler(c web.C, w http.ResponseWriter, r *http.Request) {
if err == NotFoundErr { if err == NotFoundErr {
notFoundHandler(c, w, r) notFoundHandler(c, w, r)
return return
} else if err == BadMetadata { } else if err == backends.BadMetadata {
oopsHandler(c, w, r, RespAUTO, "Corrupt metadata.") oopsHandler(c, w, r, RespAUTO, "Corrupt metadata.")
return return
} }
@ -72,7 +73,7 @@ func checkFile(filename string) error {
if expired { if expired {
fileBackend.Delete(filename) fileBackend.Delete(filename)
metaBackend.Delete(filename) metaStorageBackend.Delete(filename)
return NotFoundErr return NotFoundErr
} }

46
linx-cleanup/cleanup.go Normal file
View File

@ -0,0 +1,46 @@
package main
import (
"flag"
"log"
"github.com/andreimarcu/linx-server/backends/localfs"
"github.com/andreimarcu/linx-server/backends/metajson"
"github.com/andreimarcu/linx-server/expiry"
)
func main() {
var filesDir string
var metaDir string
var noLogs bool
flag.StringVar(&filesDir, "filespath", "files/",
"path to files directory")
flag.StringVar(&metaDir, "metapath", "meta/",
"path to metadata directory")
flag.BoolVar(&noLogs, "nologs", false,
"don't log deleted files")
flag.Parse()
metaStorageBackend := localfs.NewLocalfsBackend(metaDir)
metaBackend := metajson.NewMetaJSONBackend(metaStorageBackend)
fileBackend := localfs.NewLocalfsBackend(filesDir)
files, err := metaStorageBackend.List()
if err != nil {
panic(err)
}
for _, filename := range files {
metadata, err := metaBackend.Get(filename)
if err != nil {
log.Printf("Failed to find metadata for %s", filename)
}
if expiry.IsTsExpired(metadata.Expiry) {
log.Printf("Delete %s", filename)
fileBackend.Delete(filename)
metaStorageBackend.Delete(filename)
}
}
}

75
meta.go
View File

@ -3,46 +3,25 @@ package main
import ( import (
"archive/tar" "archive/tar"
"archive/zip" "archive/zip"
"bytes"
"compress/bzip2" "compress/bzip2"
"compress/gzip" "compress/gzip"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"encoding/json"
"errors" "errors"
"io" "io"
"sort" "sort"
"time" "time"
"unicode" "unicode"
"github.com/andreimarcu/linx-server/backends"
"github.com/andreimarcu/linx-server/expiry"
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
"gopkg.in/h2non/filetype.v1" "gopkg.in/h2non/filetype.v1"
) )
type MetadataJSON struct {
DeleteKey string `json:"delete_key"`
Sha256sum string `json:"sha256sum"`
Mimetype string `json:"mimetype"`
Size int64 `json:"size"`
Expiry int64 `json:"expiry"`
ArchiveFiles []string `json:"archive_files,omitempty"`
ShortURL string `json:"short_url"`
}
type Metadata struct {
DeleteKey string
Sha256sum string
Mimetype string
Size int64
Expiry time.Time
ArchiveFiles []string
ShortURL string
}
var NotFoundErr = errors.New("File not found.") var NotFoundErr = errors.New("File not found.")
var BadMetadata = errors.New("Corrupted metadata.")
func generateMetadata(fName string, exp time.Time, delKey string) (m Metadata, err error) { func generateMetadata(fName string, exp time.Time, delKey string) (m backends.Metadata, err error) {
file, err := fileBackend.Open(fName) file, err := fileBackend.Open(fName)
if err != nil { if err != nil {
return return
@ -145,59 +124,23 @@ func generateMetadata(fName string, exp time.Time, delKey string) (m Metadata, e
return return
} }
func metadataWrite(filename string, metadata *Metadata) error { func metadataWrite(filename string, metadata *backends.Metadata) error {
mjson := MetadataJSON{} return metaBackend.Put(filename, metadata)
mjson.DeleteKey = metadata.DeleteKey
mjson.Mimetype = metadata.Mimetype
mjson.ArchiveFiles = metadata.ArchiveFiles
mjson.Sha256sum = metadata.Sha256sum
mjson.Expiry = metadata.Expiry.Unix()
mjson.Size = metadata.Size
mjson.ShortURL = metadata.ShortURL
byt, err := json.Marshal(mjson)
if err != nil {
return err
}
if _, err := metaBackend.Put(filename, bytes.NewBuffer(byt)); err != nil {
return err
}
return nil
} }
func metadataRead(filename string) (metadata Metadata, err error) { func metadataRead(filename string) (metadata backends.Metadata, err error) {
b, err := metaBackend.Get(filename) metadata, err = metaBackend.Get(filename)
if err != nil { if err != nil {
// Metadata does not exist, generate one // Metadata does not exist, generate one
newMData, err := generateMetadata(filename, neverExpire, "") newMData, err := generateMetadata(filename, expiry.NeverExpire, "")
if err != nil { if err != nil {
return metadata, err return metadata, err
} }
metadataWrite(filename, &newMData) metadataWrite(filename, &newMData)
b, err = metaBackend.Get(filename) metadata, err = metaBackend.Get(filename)
if err != nil {
return metadata, BadMetadata
}
} }
mjson := MetadataJSON{}
err = json.Unmarshal(b, &mjson)
if err != nil {
return metadata, BadMetadata
}
metadata.DeleteKey = mjson.DeleteKey
metadata.Mimetype = mjson.Mimetype
metadata.ArchiveFiles = mjson.ArchiveFiles
metadata.Sha256sum = mjson.Sha256sum
metadata.Expiry = time.Unix(mjson.Expiry, 0)
metadata.Size = mjson.Size
metadata.ShortURL = mjson.ShortURL
return return
} }

View File

@ -16,6 +16,7 @@ import (
"github.com/GeertJohan/go.rice" "github.com/GeertJohan/go.rice"
"github.com/andreimarcu/linx-server/backends" "github.com/andreimarcu/linx-server/backends"
"github.com/andreimarcu/linx-server/backends/localfs" "github.com/andreimarcu/linx-server/backends/localfs"
"github.com/andreimarcu/linx-server/backends/metajson"
"github.com/flosch/pongo2" "github.com/flosch/pongo2"
"github.com/vharitonsky/iniflags" "github.com/vharitonsky/iniflags"
"github.com/zenazn/goji/graceful" "github.com/zenazn/goji/graceful"
@ -65,7 +66,8 @@ var staticBox *rice.Box
var timeStarted time.Time var timeStarted time.Time
var timeStartedStr string var timeStartedStr string
var remoteAuthKeys []string var remoteAuthKeys []string
var metaBackend backends.StorageBackend var metaStorageBackend backends.MetaStorageBackend
var metaBackend backends.MetaBackend
var fileBackend backends.StorageBackend var fileBackend backends.StorageBackend
func setup() *web.Mux { func setup() *web.Mux {
@ -124,7 +126,8 @@ func setup() *web.Mux {
Config.sitePath = "/" Config.sitePath = "/"
} }
metaBackend = localfs.NewLocalfsBackend(Config.metaDir) metaStorageBackend = localfs.NewLocalfsBackend(Config.metaDir)
metaBackend = metajson.NewMetaJSONBackend(metaStorageBackend)
fileBackend = localfs.NewLocalfsBackend(Config.filesDir) fileBackend = localfs.NewLocalfsBackend(Config.filesDir)
// Template setup // Template setup

View File

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/andreimarcu/linx-server/backends"
"github.com/zeebo/bencode" "github.com/zeebo/bencode"
"github.com/zenazn/goji/web" "github.com/zenazn/goji/web"
) )
@ -74,7 +75,7 @@ func fileTorrentHandler(c web.C, w http.ResponseWriter, r *http.Request) {
if err == NotFoundErr { if err == NotFoundErr {
notFoundHandler(c, w, r) notFoundHandler(c, w, r)
return return
} else if err == BadMetadata { } else if err == backends.BadMetadata {
oopsHandler(c, w, r, RespAUTO, "Corrupt metadata.") oopsHandler(c, w, r, RespAUTO, "Corrupt metadata.")
return return
} }

View File

@ -15,6 +15,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/andreimarcu/linx-server/backends"
"github.com/andreimarcu/linx-server/expiry"
"github.com/dchest/uniuri" "github.com/dchest/uniuri"
"github.com/zenazn/goji/web" "github.com/zenazn/goji/web"
"gopkg.in/h2non/filetype.v1" "gopkg.in/h2non/filetype.v1"
@ -41,7 +43,7 @@ type UploadRequest struct {
// Metadata associated with a file as it would actually be stored // Metadata associated with a file as it would actually be stored
type Upload struct { type Upload struct {
Filename string // Final filename on disk Filename string // Final filename on disk
Metadata Metadata Metadata backends.Metadata
} }
func uploadPostHandler(c web.C, w http.ResponseWriter, r *http.Request) { func uploadPostHandler(c web.C, w http.ResponseWriter, r *http.Request) {
@ -258,11 +260,11 @@ func processUpload(upReq UploadRequest) (upload Upload, err error) {
} }
// Get the rest of the metadata needed for storage // Get the rest of the metadata needed for storage
var expiry time.Time var fileExpiry time.Time
if upReq.expiry == 0 { if upReq.expiry == 0 {
expiry = neverExpire fileExpiry = expiry.NeverExpire
} else { } else {
expiry = time.Now().Add(upReq.expiry) fileExpiry = time.Now().Add(upReq.expiry)
} }
bytes, err := fileBackend.Put(upload.Filename, io.MultiReader(bytes.NewReader(header), upReq.src)) bytes, err := fileBackend.Put(upload.Filename, io.MultiReader(bytes.NewReader(header), upReq.src))
@ -273,7 +275,7 @@ func processUpload(upReq UploadRequest) (upload Upload, err error) {
return upload, errors.New("File too large") return upload, errors.New("File too large")
} }
upload.Metadata, err = generateMetadata(upload.Filename, expiry, upReq.deletionKey) upload.Metadata, err = generateMetadata(upload.Filename, fileExpiry, upReq.deletionKey)
if err != nil { if err != nil {
fileBackend.Delete(upload.Filename) fileBackend.Delete(upload.Filename)
return return
@ -342,14 +344,14 @@ func parseExpiry(expStr string) time.Duration {
if expStr == "" { if expStr == "" {
return time.Duration(Config.maxExpiry) * time.Second return time.Duration(Config.maxExpiry) * time.Second
} else { } else {
expiry, err := strconv.ParseUint(expStr, 10, 64) fileExpiry, err := strconv.ParseUint(expStr, 10, 64)
if err != nil { if err != nil {
return time.Duration(Config.maxExpiry) * time.Second return time.Duration(Config.maxExpiry) * time.Second
} else { } else {
if Config.maxExpiry > 0 && (expiry > Config.maxExpiry || expiry == 0) { if Config.maxExpiry > 0 && (fileExpiry > Config.maxExpiry || fileExpiry == 0) {
expiry = Config.maxExpiry fileExpiry = Config.maxExpiry
} }
return time.Duration(expiry) * time.Second return time.Duration(fileExpiry) * time.Second
} }
} }
} }