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.
type FileInfo struct {
*Listing
Fs afero.Fs `json:"-"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,omitempty"`
Content string `json:"content,omitempty"`
Checksums map[string]string `json:"checksums,omitempty"`
Token string `json:"token,omitempty"`
Fs afero.Fs `json:"-"`
Dir string `json:"dir"`
Path string `json:"path"`
Name string `json:"name"`
Size int64 `json:"size"`
Extension string `json:"extension"`
ModTime time.Time `json:"modified"`
Mode os.FileMode `json:"mode"`
IsDir bool `json:"isDir"`
IsSymlink bool `json:"isSymlink"`
IsThumbsEnabled bool `json:"isThumbsEnabled"`
Type string `json:"type"`
Subtitles []string `json:"subtitles,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.
@ -54,6 +56,11 @@ type FileOptions struct {
Content bool
}
type FileThumbnail struct {
Dir string
Path string
}
// 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
// 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
}
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) {
var file *FileInfo
@ -92,8 +127,10 @@ func stat(opts FileOptions) (*FileInfo, error) {
if err != nil {
return nil, err
}
dir, _ := filepath.Split(opts.Path)
file = &FileInfo{
Fs: opts.Fs,
Dir: dir,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
@ -128,8 +165,10 @@ func stat(opts FileOptions) (*FileInfo, error) {
return file, nil
}
dir, _ := filepath.Split(opts.Path)
file = &FileInfo{
Fs: opts.Fs,
Dir: dir,
Path: opts.Path,
Name: info.Name(),
ModTime: info.ModTime(),
@ -224,12 +263,14 @@ func (i *FileInfo) detectType(modify, saveContent, readHeader bool) error {
switch {
case strings.HasPrefix(mimetype, "video"):
i.Type = "video"
i.detectThumbnail()
i.detectSubtitles()
return nil
case strings.HasPrefix(mimetype, "audio"):
i.Type = "audio"
return nil
case strings.HasPrefix(mimetype, "image"):
i.IsThumbsEnabled = true
i.Type = "image"
return nil
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 {
afs := &afero.Afero{Fs: i.Fs}
dir, err := afs.ReadDir(i.Path)

View File

@ -6,10 +6,7 @@ import (
"github.com/spf13/afero"
)
// 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 {
func CreateDir(fs afero.Fs, source, dest string) error {
// Get properties of source.
srcinfo, err := fs.Stat(source)
if err != nil {
@ -22,6 +19,18 @@ func CopyDir(fs afero.Fs, source, dest string) error {
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)
obs, err := dir.Readdir(-1)
if err != nil {

View File

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

View File

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

View File

@ -59,6 +59,18 @@ func previewHandler(imgSvc ImgService, fileCache FileCache, enableThumbnails, re
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 {
case "image":
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
}
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 {
return d.user.Fs.RemoveAll(r.URL.Path)
}, "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
}
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)
case "rename":
if !d.user.Perm.Rename {
@ -325,6 +363,32 @@ func patchAction(ctx context.Context, action, src, dst string, d *data, fileCach
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)
default:
return fmt.Errorf("unsupported action %s: %w", action, errors.ErrInvalidRequestParams)