feat: added thumbnail support for videos

This commit is contained in:
Cy Allen Scott 2022-04-21 18:16:24 -04:00
parent 040584c865
commit 6c22cb3b32
6 changed files with 165 additions and 21 deletions

View File

@ -26,20 +26,22 @@ import (
// FileInfo describes a file. // FileInfo describes a file.
type FileInfo struct { type FileInfo struct {
*Listing *Listing
Fs afero.Fs `json:"-"` Fs afero.Fs `json:"-"`
Path string `json:"path"` Dir string `json:"dir"`
Name string `json:"name"` Path string `json:"path"`
Size int64 `json:"size"` Name string `json:"name"`
Extension string `json:"extension"` Size int64 `json:"size"`
ModTime time.Time `json:"modified"` Extension string `json:"extension"`
Mode os.FileMode `json:"mode"` ModTime time.Time `json:"modified"`
IsDir bool `json:"isDir"` Mode os.FileMode `json:"mode"`
IsSymlink bool `json:"isSymlink"` IsDir bool `json:"isDir"`
Type string `json:"type"` IsSymlink bool `json:"isSymlink"`
Subtitles []string `json:"subtitles,omitempty"` IsThumbsEnabled bool `json:"isThumbsEnabled"`
Content string `json:"content,omitempty"` Type string `json:"type"`
Checksums map[string]string `json:"checksums,omitempty"` Subtitles []string `json:"subtitles,omitempty"`
Token string `json:"token,omitempty"` Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
} }
// FileOptions are the options when getting a file info. // FileOptions are the options when getting a file info.
@ -54,6 +56,11 @@ type FileOptions struct {
Content bool Content bool
} }
type FileThumbnail struct {
Dir string
Path string
}
// NewFileInfo creates a File object from a path and a given user. This File // NewFileInfo creates a File object from a path and a given user. This File
// object will be automatically filled depending on if it is a directory // object will be automatically filled depending on if it is a directory
// or a file. If it's a video file, it will also detect any subtitles. // or a file. If it's a video file, it will also detect any subtitles.
@ -84,6 +91,34 @@ func NewFileInfo(opts FileOptions) (*FileInfo, error) {
return file, err return file, err
} }
func NewThumbnailInfo(opts FileOptions) (*FileInfo, error) {
return NewFileInfo(FileOptions{
Fs: opts.Fs,
Path: NewFileThumbnail(opts).Path,
Modify: opts.Modify,
Expand: opts.Expand,
ReadHeader: opts.ReadHeader,
Checker: opts.Checker,
})
}
func NewFileThumbnail(opts FileOptions) FileThumbnail {
dir, name := filepath.Split(opts.Path)
hash := md5.Sum([]byte(name))
thumbnailName := hex.EncodeToString(hash[:]) + ".jpg"
thumbnailPath := path.Join(dir, ".filebrowser", thumbnailName)
dir, _ = filepath.Split(thumbnailPath)
return FileThumbnail{
Dir: dir,
Path: thumbnailPath,
}
}
func stat(opts FileOptions) (*FileInfo, error) { func stat(opts FileOptions) (*FileInfo, error) {
var file *FileInfo var file *FileInfo
@ -92,8 +127,10 @@ func stat(opts FileOptions) (*FileInfo, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
dir, _ := filepath.Split(opts.Path)
file = &FileInfo{ file = &FileInfo{
Fs: opts.Fs, Fs: opts.Fs,
Dir: dir,
Path: opts.Path, Path: opts.Path,
Name: info.Name(), Name: info.Name(),
ModTime: info.ModTime(), ModTime: info.ModTime(),
@ -128,8 +165,10 @@ func stat(opts FileOptions) (*FileInfo, error) {
return file, nil return file, nil
} }
dir, _ := filepath.Split(opts.Path)
file = &FileInfo{ file = &FileInfo{
Fs: opts.Fs, Fs: opts.Fs,
Dir: dir,
Path: opts.Path, Path: opts.Path,
Name: info.Name(), Name: info.Name(),
ModTime: info.ModTime(), ModTime: info.ModTime(),
@ -224,12 +263,14 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
switch { switch {
case strings.HasPrefix(mimetype, "video"): case strings.HasPrefix(mimetype, "video"):
i.Type = "video" i.Type = "video"
i.detectThumbnail()
i.detectSubtitles() i.detectSubtitles()
return nil return nil
case strings.HasPrefix(mimetype, "audio"): case strings.HasPrefix(mimetype, "audio"):
i.Type = "audio" i.Type = "audio"
return nil return nil
case strings.HasPrefix(mimetype, "image"): case strings.HasPrefix(mimetype, "image"):
i.IsThumbsEnabled = true
i.Type = "image" i.Type = "image"
return nil return nil
case strings.HasSuffix(mimetype, "pdf"): case strings.HasSuffix(mimetype, "pdf"):
@ -301,6 +342,21 @@ func (i *FileInfo) detectSubtitles() {
} }
} }
func (i *FileInfo) detectThumbnail() {
dir, name := filepath.Split(i.RealPath())
hash := md5.Sum([]byte(name))
thumbnailName := hex.EncodeToString(hash[:])
path := path.Join(dir, ".filebrowser", thumbnailName+".jpg")
_, err := os.Stat(path)
if err == nil {
i.IsThumbsEnabled = true
}
}
func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error { func (i *FileInfo) readListing(checker rules.Checker, readHeader bool) error {
afs := &afero.Afero{Fs: i.Fs} afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path) dir, err := afs.ReadDir(i.Path)

View File

@ -6,10 +6,7 @@ import (
"github.com/spf13/afero" "github.com/spf13/afero"
) )
// CopyDir copies a directory from source to dest and all func CreateDir(fs afero.Fs, source, dest string) error {
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source, dest string) error {
// Get properties of source. // Get properties of source.
srcinfo, err := fs.Stat(source) srcinfo, err := fs.Stat(source)
if err != nil { if err != nil {
@ -22,6 +19,18 @@ func CopyDir(fs afero.Fs, source, dest string) error {
return err return err
} }
return nil
}
// CopyDir copies a directory from source to dest and all
// of its sub-directories. It doesn't stop if it finds an error
// during the copy. Returns an error if any.
func CopyDir(fs afero.Fs, source, dest string) error {
err := CreateDir(fs, source, dest)
if err != nil {
return err
}
dir, _ := fs.Open(source) dir, _ := fs.Open(source)
obs, err := dir.Readdir(-1) obs, err := dir.Readdir(-1)
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@
@dragover="dragOver" @dragover="dragOver"
@drop="drop" @drop="drop"
@click="itemClick" @click="itemClick"
:data-thumbs-enabled="isThumbsEnabled"
:data-dir="isDir" :data-dir="isDir"
:data-type="type" :data-type="type"
:aria-label="name" :aria-label="name"
@ -15,7 +16,7 @@
> >
<div> <div>
<img <img
v-if="readOnly == undefined && type === 'image' && isThumbsEnabled" v-if="hasThumbnailUrl"
v-lazy="thumbnailUrl" v-lazy="thumbnailUrl"
/> />
<i v-else class="material-icons"></i> <i v-else class="material-icons"></i>
@ -52,6 +53,7 @@ export default {
props: [ props: [
"name", "name",
"isDir", "isDir",
"isThumbsEnabled",
"url", "url",
"type", "type",
"size", "size",
@ -90,8 +92,8 @@ export default {
return `${baseURL}/api/preview/thumb/${path}?k=${key}&inline=true`; return `${baseURL}/api/preview/thumb/${path}?k=${key}&inline=true`;
}, },
isThumbsEnabled() { hasThumbnailUrl() {
return enableThumbs; return this.readOnly == undefined && this.isThumbsEnabled && enableThumbs;
}, },
}, },
methods: { methods: {

View File

@ -221,6 +221,7 @@
v-bind:index="item.index" v-bind:index="item.index"
v-bind:name="item.name" v-bind:name="item.name"
v-bind:isDir="item.isDir" v-bind:isDir="item.isDir"
v-bind:isThumbsEnabled="item.isThumbsEnabled"
v-bind:url="item.url" v-bind:url="item.url"
v-bind:modified="item.modified" v-bind:modified="item.modified"
v-bind:type="item.type" v-bind:type="item.type"

View File

@ -59,6 +59,18 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
setContentDisposition(w, r, file) setContentDisposition(w, r, file)
thumbnail, err := files.NewThumbnailInfo(files.FileOptions{
Fs: d.user.Fs,
Path: "/" + vars["path"],
Modify: d.user.Perm.Modify,
Expand: true,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err == nil && thumbnail != nil {
return rawFileHandler(w, r, thumbnail)
}
switch file.Type { switch file.Type {
case "image": case "image":
return handleImagePreview(w, r, imgSvc, fileCache, file, previewSize, enableThumbnails, resizePreview) return handleImagePreview(w, r, imgSvc, fileCache, file, previewSize, enableThumbnails, resizePreview)

View File

@ -77,6 +77,18 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc {
return errToStatus(err), err return errToStatus(err), err
} }
thumbnail, err := files.NewThumbnailInfo(files.FileOptions{
Fs: d.user.Fs,
Path: r.URL.Path,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err == nil && thumbnail != nil {
d.user.Fs.RemoveAll(thumbnail.Path)
}
err = d.RunHook(func() error { err = d.RunHook(func() error {
return d.user.Fs.RemoveAll(r.URL.Path) return d.user.Fs.RemoveAll(r.URL.Path)
}, "delete", r.URL.Path, "", d.user) }, "delete", r.URL.Path, "", d.user)
@ -299,6 +311,32 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return errors.ErrPermissionDenied return errors.ErrPermissionDenied
} }
srcThumbnail, err := files.NewThumbnailInfo(files.FileOptions{
Fs: d.user.Fs,
Path: src,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err == nil && srcThumbnail != nil {
destThumbnail := files.NewFileThumbnail(files.FileOptions{
Fs: d.user.Fs,
Path: dst,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
_, err := os.Stat(destThumbnail.Dir)
if err != nil {
fileutils.CreateDir(d.user.Fs, srcThumbnail.Dir, destThumbnail.Dir)
}
fileutils.Copy(d.user.Fs, srcThumbnail.Path, destThumbnail.Path)
}
return fileutils.Copy(d.user.Fs, src, dst) return fileutils.Copy(d.user.Fs, src, dst)
case "rename": case "rename":
if !d.user.Perm.Rename { if !d.user.Perm.Rename {
@ -325,6 +363,32 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
return err return err
} }
srcThumbnail, err := files.NewThumbnailInfo(files.FileOptions{
Fs: d.user.Fs,
Path: src,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
if err == nil && srcThumbnail != nil {
destThumbnail := files.NewFileThumbnail(files.FileOptions{
Fs: d.user.Fs,
Path: dst,
Modify: d.user.Perm.Modify,
Expand: false,
ReadHeader: d.server.TypeDetectionByHeader,
Checker: d,
})
_, err := os.Stat(destThumbnail.Dir)
if err != nil {
fileutils.CreateDir(d.user.Fs, srcThumbnail.Dir, destThumbnail.Dir)
}
fileutils.MoveFile(d.user.Fs, srcThumbnail.Path, destThumbnail.Path)
}
return fileutils.MoveFile(d.user.Fs, src, dst) return fileutils.MoveFile(d.user.Fs, src, dst)
default: default:
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams) return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)