Compare commits
11 Commits
v1.6.1
...
minor-bug-
| Author | SHA1 | Date | |
|---|---|---|---|
| 806ee9edd2 | |||
| 862f562a83 | |||
| a83a34a047 | |||
| 3cf3a5ae06 | |||
| 7bf72f2848 | |||
| a9031c8a2b | |||
| e891c77bce | |||
| da5f8b14b1 | |||
| d6d768980b | |||
| 7d82cd8d57 | |||
| 02bc633f1b |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,10 @@
|
|||||||
cmd/contented/contented
|
# Ignore all files when running locally
|
||||||
|
cmd/contented/*
|
||||||
|
# But add back the actual main file
|
||||||
|
!cmd/contented/main.go
|
||||||
cmd/contented-multi/contented-multi
|
cmd/contented-multi/contented-multi
|
||||||
build/
|
build/
|
||||||
_dist/
|
_dist/
|
||||||
|
# Ignore any built files if they ever turn up
|
||||||
contented.db
|
contented.db
|
||||||
|
contented.exe
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ $.get("/about", function(ret) {
|
|||||||
|
|
||||||
// Load upload widget
|
// Load upload widget
|
||||||
contented.init("#surrogate-area", function(items) {
|
contented.init("#surrogate-area", function(items) {
|
||||||
window.location.href = contented.getMultiPreviewURL(items);
|
if (items.length == 1) {
|
||||||
|
window.location.href = contented.getDownloadURL(items[0]);
|
||||||
|
} else {
|
||||||
|
window.location.href = contented.getMultiPreviewURL(items);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -273,17 +273,20 @@
|
|||||||
var $element = $(element);
|
var $element = $(element);
|
||||||
var offset = $element.offset();
|
var offset = $element.offset();
|
||||||
|
|
||||||
$f.css({
|
onresize = (_) => {
|
||||||
'position': 'absolute',
|
$f.css({
|
||||||
'left': offset.left + "px",
|
'position': 'absolute',
|
||||||
'top': offset.top + "px",
|
'left': offset.left + "px",
|
||||||
'width': $element.width() + "px",
|
'top': offset.top + "px",
|
||||||
'min-width': $element.width() + "px",
|
'width': $element.width() + "px",
|
||||||
'max-width': $element.width() + "px",
|
'min-width': $element.width() + "px",
|
||||||
'height': $element.height() + "px",
|
'max-width': $element.width() + "px",
|
||||||
'min-height': $element.height() + "px",
|
'height': $element.height() + "px",
|
||||||
'max-height': $element.height() + "px"
|
'min-height': $element.height() + "px",
|
||||||
});
|
'max-height': $element.height() + "px"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
onresize();
|
||||||
|
|
||||||
// Drag and drop support
|
// Drag and drop support
|
||||||
|
|
||||||
|
|||||||
92
storage.go
92
storage.go
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,9 +14,16 @@ import (
|
|||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type cleanupFunc func() error
|
||||||
|
|
||||||
type Storage interface {
|
type Storage interface {
|
||||||
ReadFile(ctx context.Context, fileHash string) (io.ReadSeekCloser, error)
|
ReadFile(ctx context.Context, fileHash string) (io.ReadSeekCloser, error)
|
||||||
SaveFile(ctx context.Context, fileHash string, srcLen int64, src io.Reader) error
|
SaveFile(ctx context.Context, fileHash string, srcLen int64, src io.Reader) error
|
||||||
|
|
||||||
|
// TempLocalFilepath returns a local file path and a cleanup function that
|
||||||
|
// can be used to access the file through the filesystem.
|
||||||
|
// It's an alternative to ReadFile.
|
||||||
|
TempLocalFilepath(ctx context.Context, fileHash string) (string, cleanupFunc, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -63,6 +71,19 @@ func (ls *localStorage) SaveFile(ctx context.Context, fileHash string, srcLen in
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ls *localStorage) TempLocalFilepath(ctx context.Context, fileHash string) (string, cleanupFunc, error) {
|
||||||
|
path := filepath.Join(ls.dataDir, fileHash)
|
||||||
|
cleanup := func() error { return nil }
|
||||||
|
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return "", nil, err // e.g. not exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists (but may be TOCTTOU)
|
||||||
|
|
||||||
|
return path, cleanup, nil
|
||||||
|
}
|
||||||
|
|
||||||
var _ Storage = &localStorage{} // interface assertion
|
var _ Storage = &localStorage{} // interface assertion
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -100,7 +121,36 @@ func (ss *s3Storage) ReadFile(ctx context.Context, fileHash string) (io.ReadSeek
|
|||||||
func (ss *s3Storage) SaveFile(ctx context.Context, fileHash string, srcLen int64, src io.Reader) error {
|
func (ss *s3Storage) SaveFile(ctx context.Context, fileHash string, srcLen int64, src io.Reader) error {
|
||||||
_, err := ss.s3client.PutObject(ctx, ss.Bucket, ss.Prefix+fileHash, src, srcLen, minio.PutObjectOptions{})
|
_, err := ss.s3client.PutObject(ctx, ss.Bucket, ss.Prefix+fileHash, src, srcLen, minio.PutObjectOptions{})
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ss *s3Storage) TempLocalFilepath(ctx context.Context, fileHash string) (string, cleanupFunc, error) {
|
||||||
|
|
||||||
|
// Download to temporary file
|
||||||
|
fh, err := os.CreateTemp("", "contented-temp-*")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
name := fh.Name() // n.b. This is the absolute path to the file
|
||||||
|
|
||||||
|
r, err := ss.ReadFile(ctx, fileHash)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(fh, r)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = r.Close()
|
||||||
|
_ = fh.Close()
|
||||||
|
|
||||||
|
cleanup := func() error {
|
||||||
|
return os.Remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return name, cleanup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Storage = &s3Storage{} // interface assertion
|
var _ Storage = &s3Storage{} // interface assertion
|
||||||
@@ -155,20 +205,39 @@ func (ts *tieredStorage) migrateNow() error {
|
|||||||
return fmt.Errorf("Reading hot storage files: %w", err)
|
return fmt.Errorf("Reading hot storage files: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(dirents) == 0 {
|
||||||
|
return nil // Directory empty, nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
cutOff := time.Now().Add(-TierMigrationAfter)
|
cutOff := time.Now().Add(-TierMigrationAfter)
|
||||||
|
|
||||||
|
// Shuffle files to avoid getting stuck
|
||||||
|
rand.Shuffle(len(dirents), func(i, j int) {
|
||||||
|
dirents[i], dirents[j] = dirents[j], dirents[i]
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("tier-migration: Scanning %d items...", len(dirents))
|
||||||
|
|
||||||
|
var countMigrated int64 = 0
|
||||||
|
|
||||||
for _, dirent := range dirents {
|
for _, dirent := range dirents {
|
||||||
fi, err := dirent.Info()
|
fi, err := dirent.Info()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Reading hot storage files: %w", err) // local files can't be stat'd = important error
|
return fmt.Errorf("Reading hot storage files: %w", err) // local files can't be stat'd = important error
|
||||||
}
|
}
|
||||||
|
|
||||||
if !fi.ModTime().After(cutOff) {
|
if fi.IsDir() {
|
||||||
|
continue // probably . or ..
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi.ModTime().After(cutOff) {
|
||||||
continue // not eligible
|
continue // not eligible
|
||||||
}
|
}
|
||||||
|
|
||||||
fileHash := dirent.Name()
|
fileHash := dirent.Name()
|
||||||
|
|
||||||
|
log.Printf("tier-migration: Migrating %q...", fileHash)
|
||||||
|
|
||||||
// Copy to cold storage
|
// Copy to cold storage
|
||||||
// Any concurrent reads will be serviced from the hot storage, so this
|
// Any concurrent reads will be serviced from the hot storage, so this
|
||||||
// is a safe operation
|
// is a safe operation
|
||||||
@@ -186,11 +255,15 @@ func (ts *tieredStorage) migrateNow() error {
|
|||||||
// Copy was successful. Delete local file
|
// Copy was successful. Delete local file
|
||||||
err = os.Remove(filepath.Join(ts.hot.dataDir, fileHash))
|
err = os.Remove(filepath.Join(ts.hot.dataDir, fileHash))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Remove %q from hot storage: %w", err) // can't rm local file
|
return fmt.Errorf("Remove %q from hot storage: %w", fileHash, err) // can't rm local file
|
||||||
}
|
}
|
||||||
|
|
||||||
|
countMigrated++
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrated everything we can for now
|
// Migrated everything we can for now
|
||||||
|
log.Printf("tier-migration: Sleeping (migrated %d/%d items)", countMigrated, len(dirents))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,4 +279,19 @@ func (ts *tieredStorage) SaveFile(ctx context.Context, fileHash string, srcLen i
|
|||||||
return ts.hot.SaveFile(ctx, fileHash, srcLen, src)
|
return ts.hot.SaveFile(ctx, fileHash, srcLen, src)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ts *tieredStorage) TempLocalFilepath(ctx context.Context, fileHash string) (string, cleanupFunc, error) {
|
||||||
|
path, cleanup, err_hot := ts.hot.TempLocalFilepath(ctx, fileHash)
|
||||||
|
if err_hot == nil {
|
||||||
|
return path, cleanup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
path, cleanup, err_cold := ts.cold.TempLocalFilepath(ctx, fileHash)
|
||||||
|
if err_cold == nil {
|
||||||
|
return path, cleanup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neither err_hot nor err_cold worked
|
||||||
|
return "", nil, fmt.Errorf("TempLocalFilepath: failed to allocate local path for %q (hot error: %q, cold error: %q)", fileHash, err_hot.Error(), err_cold.Error())
|
||||||
|
}
|
||||||
|
|
||||||
var _ Storage = &tieredStorage{} // interface assertion
|
var _ Storage = &tieredStorage{} // interface assertion
|
||||||
|
|||||||
46
thumb.go
46
thumb.go
@@ -4,11 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"code.ivysaur.me/thumbnail"
|
"code.ivysaur.me/thumbnail"
|
||||||
)
|
)
|
||||||
@@ -93,45 +90,30 @@ func (this *Server) handleThumbInternal(ctx context.Context, w http.ResponseWrit
|
|||||||
// Load metadata
|
// Load metadata
|
||||||
m, err := this.Metadata(fileId)
|
m, err := this.Metadata(fileId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("Metadata: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.FileSize > this.opts.MaxThumbSizeBytes {
|
if m.FileSize > this.opts.MaxThumbSizeBytes {
|
||||||
return errors.New("Don't want to thumbnail very large files, sorry")
|
return errors.New("Don't want to thumbnail very large files, sorry")
|
||||||
}
|
}
|
||||||
|
|
||||||
var filePath string
|
// Some storage backends can supply a local filepath immediately for thumbnailing
|
||||||
|
// Others (s3, tiered) need to download the file temporarily first
|
||||||
if this.opts.StorageType == STORAGE_LOCAL {
|
filePath, cleanup, err := this.store.TempLocalFilepath(ctx, m.FileHash)
|
||||||
filePath = filepath.Join(this.opts.DataDirectory, m.FileHash)
|
if err != nil {
|
||||||
|
return fmt.Errorf("TempLocalFilepath: %w", err)
|
||||||
} else if this.opts.StorageType == STORAGE_S3 {
|
|
||||||
// Need to temporarily download it for thumbnailing (slow and costs money)
|
|
||||||
|
|
||||||
destFh, err := os.CreateTemp("", "contented-thumbcache-*")
|
|
||||||
defer os.Remove(destFh.Name())
|
|
||||||
|
|
||||||
srcFh, err := this.store.ReadFile(ctx, m.FileHash)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.CopyN(destFh, srcFh, m.FileSize)
|
|
||||||
srcFh.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
destFh.Seek(0, io.SeekStart)
|
|
||||||
filePath = destFh.Name()
|
|
||||||
|
|
||||||
} else {
|
|
||||||
panic("bad StorageType")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// delete temporary file, if necessary
|
||||||
|
if cleanupErr := cleanup(); cleanupErr != nil {
|
||||||
|
log.Printf("cleaning up temporary thumbnail file: %v", cleanupErr)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
thumb, err := t.RenderFileAs(filePath, m.MimeType)
|
thumb, err := t.RenderFileAs(filePath, m.MimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("RenderFileAs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set(`Cache-Control`, `max-age=31536000, immutable`)
|
w.Header().Set(`Cache-Control`, `max-age=31536000, immutable`)
|
||||||
|
|||||||
Reference in New Issue
Block a user