Sync: Ignore unsupported file types #225

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-04-07 10:42:42 +02:00
parent 924eeac55c
commit b020b4e415
27 changed files with 461 additions and 387 deletions

View file

@ -31,4 +31,4 @@ INSERT INTO labels (id, label_uuid, label_slug, label_name, label_priority, labe
INSERT INTO labels (id, label_uuid, label_slug, label_name, label_priority, label_favorite) VALUES ('3', '14', 'cow', 'COW', -1, 1);
INSERT INTO photos_labels (photo_id, label_id, label_uncertainty, label_source) VALUES ('1', '1', '38', 'image');
INSERT INTO photos_labels (photo_id, label_id, label_uncertainty, label_source) VALUES ('1', '2', '10', 'image');
INSERT INTO accounts (id, acc_name, acc_owner, acc_url, acc_type, acc_key, acc_user, acc_pass, acc_error, acc_share, acc_sync, retry_limit, share_path, share_size, share_expires, sync_path, sync_interval, sync_upload, sync_download, sync_raw, sync_video, sync_sidecar, created_at, updated_at, deleted_at) VALUES (1, 'Test Account', 'Admin', 'http://webdav-dummy/', 'webdav', '', 'admin', 'photoprism', null, true, false, 3, '/Photos', null, null, null, null, null, null, null, null, null, '2020-03-06 02:06:51', '2020-03-28 14:06:00', null);
INSERT INTO accounts (id, acc_name, acc_owner, acc_url, acc_type, acc_key, acc_user, acc_pass, acc_error, acc_share, acc_sync, retry_limit, share_path, share_size, share_expires, sync_path, sync_interval, sync_upload, sync_download, sync_raw, created_at, updated_at, deleted_at) VALUES (1, 'Test Account', 'Admin', 'http://webdav-dummy/', 'webdav', '', 'admin', 'photoprism', null, true, false, 3, '/Photos', null, null, null, null, null, null, null, '2020-03-06 02:06:51', '2020-03-28 14:06:00', null);

View file

@ -181,24 +181,6 @@
v-model="model.SyncRaw"
></v-checkbox>
</v-flex>
<v-flex xs12 sm6 class="px-2">
<v-checkbox
:disabled="!model.AccSync"
hide-details
color="secondary-dark"
:label="label.SyncVideo"
v-model="model.SyncVideo"
></v-checkbox>
</v-flex>
<v-flex xs12 sm6 class="px-2">
<v-checkbox
:disabled="!model.AccSync"
hide-details
color="secondary-dark"
:label="label.SyncSidecar"
v-model="model.SyncSidecar"
></v-checkbox>
</v-flex>
</v-layout>
<v-layout row wrap v-else>
<v-flex xs12 class="pa-2">
@ -367,14 +349,12 @@
ShareExpires: this.$gettext("Expires"),
SyncPath: this.$gettext("Location"),
SyncInterval: this.$gettext("Interval"),
SyncFilenames: this.$gettext("Preserve remote filenames"),
SyncFilenames: this.$gettext("Preserve filenames"),
SyncStart: this.$gettext("Start"),
SyncDownload: this.$gettext("Download remote files"),
SyncUpload: this.$gettext("Upload local files"),
SyncDelete: this.$gettext("Remote delete"),
SyncRaw: this.$gettext("Sync RAW images"),
SyncVideo: this.$gettext("Sync videos"),
SyncSidecar: this.$gettext("Sync sidecar files"),
SyncRaw: this.$gettext("Sync raw images"),
}
}
},

View file

@ -24,12 +24,10 @@ class Account extends Abstract {
SyncStatus: "",
SyncInterval: 86400,
SyncDate: null,
SyncFilenames: false,
SyncFilenames: true,
SyncUpload: false,
SyncDownload: true,
SyncRaw: true,
SyncVideo: true,
SyncSidecar: true,
CreatedAt: "",
UpdatedAt: "",
DeletedAt: null,

View file

@ -38,7 +38,7 @@ func BatchPhotosArchive(router *gin.RouterGroup, conf *config.Config) {
return
}
log.Infof("deleting photos: %#v", f.Photos)
log.Infof("photos: archiving %#v", f.Photos)
db := conf.Db()
@ -115,7 +115,7 @@ func BatchAlbumsDelete(router *gin.RouterGroup, conf *config.Config) {
return
}
log.Infof("deleting albums: %#v", f.Albums)
log.Infof("albums: deleting %#v", f.Albums)
db := conf.Db()
@ -223,7 +223,7 @@ func BatchLabelsDelete(router *gin.RouterGroup, conf *config.Config) {
return
}
log.Infof("deleting labels: %#v", f.Labels)
log.Infof("labels: deleting %#v", f.Labels)
db := conf.Db()

View file

@ -157,6 +157,8 @@ func (c *Config) connectToDatabase(ctx context.Context) error {
func (c *Config) DropTables() {
db := c.Db()
logLevel := log.Level
log.SetLevel(logrus.FatalLevel)
db.SetLogger(log)
db.LogMode(false)
@ -183,6 +185,8 @@ func (c *Config) DropTables() {
&entity.Keyword{},
&entity.PhotoKeyword{},
)
log.SetLevel(logLevel)
}
// ImportSQL imports a file to the currently configured database.

View file

@ -46,8 +46,6 @@ type Account struct {
SyncDownload bool
SyncFilenames bool
SyncRaw bool
SyncVideo bool
SyncSidecar bool
CreatedAt time.Time `deepcopier:"skip"`
UpdatedAt time.Time `deepcopier:"skip"`
DeletedAt *time.Time `deepcopier:"skip" sql:"index"`

View file

@ -9,15 +9,16 @@ import (
const (
FileSyncNew = "new"
FileSyncIgnore = "ignore"
FileSyncDownloaded = "downloaded"
FileSyncUploaded = "uploaded"
)
// FileSync represents a one-to-many relation between File and Account for syncing with remote services.
type FileSync struct {
FileID uint `gorm:"index;"`
RemoteName string `gorm:"primary_key;auto_increment:false;type:varbinary(256)"`
AccountID uint `gorm:"primary_key;auto_increment:false"`
RemoteName string `gorm:"type:varbinary(256);primary_key;auto_increment:false"`
FileID uint `gorm:"index;"`
RemoteDate time.Time
RemoteSize int64
Status string `gorm:"type:varbinary(16);"`

View file

@ -27,8 +27,6 @@ type Account struct {
SyncDownload bool `json:"SyncDownload"`
SyncFilenames bool `json:"SyncFilenames"`
SyncRaw bool `json:"SyncRaw"`
SyncVideo bool `json:"SyncVideo"`
SyncSidecar bool `json:"SyncSidecar"`
}
func NewAccount(m interface{}) (f Account, err error) {

View file

@ -107,7 +107,7 @@ func (c *Convert) ConvertCommand(image *MediaFile, jpegName string, xmpName stri
} else if image.IsHEIF() {
result = exec.Command(c.conf.HeifConvertBin(), image.fileName, jpegName)
} else {
return nil, useMutex, fmt.Errorf("convert: image type not supported for conversion (%s)", image.Type())
return nil, useMutex, fmt.Errorf("convert: image type not supported for conversion (%s)", image.FileType())
}
return result, useMutex, nil
@ -148,7 +148,7 @@ func (c *Convert) ToJpeg(image *MediaFile) (*MediaFile, error) {
}
event.Publish("index.converting", event.Data{
"fileType": image.Type(),
"fileType": image.FileType(),
"fileName": fileName,
"baseName": filepath.Base(fileName),
"xmpName": filepath.Base(xmpName),

View file

@ -47,9 +47,9 @@ func ImportWorker(jobs <-chan ImportJob) {
if related.Main.HasSameName(f) {
destinationMainFilename = destinationFilename
log.Infof("import: moving main %s file \"%s\" to \"%s\"", f.Type(), relativeFilename, destinationFilename)
log.Infof("import: moving main %s file \"%s\" to \"%s\"", f.FileType(), relativeFilename, destinationFilename)
} else {
log.Infof("import: moving related %s file \"%s\" to \"%s\"", f.Type(), relativeFilename, destinationFilename)
log.Infof("import: moving related %s file \"%s\" to \"%s\"", f.FileType(), relativeFilename, destinationFilename)
}
if opt.Move {
@ -106,7 +106,7 @@ func ImportWorker(jobs <-chan ImportJob) {
if related.Main != nil {
res := ind.MediaFile(related.Main, indexOpt, originalName)
log.Infof("import: %s main %s file \"%s\"", res, related.Main.Type(), related.Main.RelativeName(ind.originalsPath()))
log.Infof("import: %s main %s file \"%s\"", res, related.Main.FileType(), related.Main.RelativeName(ind.originalsPath()))
done[related.Main.FileName()] = true
} else {
log.Warnf("import: no main file for %s (conversion to jpeg failed?)", destinationMainFilename)
@ -124,7 +124,7 @@ func ImportWorker(jobs <-chan ImportJob) {
res := ind.MediaFile(f, indexOpt, "")
done[f.FileName()] = true
log.Infof("import: %s related %s file \"%s\"", res, f.Type(), f.RelativeName(ind.originalsPath()))
log.Infof("import: %s related %s file \"%s\"", res, f.FileType(), f.RelativeName(ind.originalsPath()))
}
}
}

View file

@ -278,7 +278,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.FileHash = fileHash
file.FileSize = fileSize
file.FileModified = fileModified
file.FileType = string(m.Type())
file.FileType = string(m.FileType())
file.FileMime = m.MimeType()
file.FileOrientation = m.Orientation()

View file

@ -18,7 +18,7 @@ func IndexWorker(jobs <-chan IndexJob) {
res := ind.MediaFile(related.Main, opt, "")
done[related.Main.FileName()] = true
log.Infof("index: %s main %s file \"%s\"", res, related.Main.Type(), related.Main.RelativeName(ind.originalsPath()))
log.Infof("index: %s main %s file \"%s\"", res, related.Main.FileType(), related.Main.RelativeName(ind.originalsPath()))
} else {
log.Warnf("index: no main file for %s (conversion to jpeg failed?)", job.FileName)
}
@ -31,7 +31,7 @@ func IndexWorker(jobs <-chan IndexJob) {
res := ind.MediaFile(f, opt, "")
done[f.FileName()] = true
log.Infof("index: %s related %s file \"%s\"", res, f.Type(), f.RelativeName(ind.originalsPath()))
log.Infof("index: %s related %s file \"%s\"", res, f.FileType(), f.RelativeName(ind.originalsPath()))
}
}
}

View file

@ -24,7 +24,7 @@ import (
// MediaFile represents a single photo, video or sidecar file.
type MediaFile struct {
fileName string
fileType fs.Type
fileType fs.FileType
mimeType string
dateCreated time.Time
hash string
@ -488,38 +488,43 @@ func (m MediaFile) IsJpeg() bool {
return m.MimeType() == fs.MimeTypeJpeg
}
// Type returns the type of the media file.
func (m MediaFile) Type() fs.Type {
return fs.Ext[m.Extension()]
// FileType returns the file type (jpg, gif, tiff,...).
func (m MediaFile) FileType() fs.FileType {
return fs.GetFileType(m.fileName)
}
// HasType returns true if this media file is of a given type.
func (m MediaFile) HasType(t fs.Type) bool {
// MediaType returns the media type (video, image, raw, sidecar,...).
func (m MediaFile) MediaType() fs.MediaType {
return fs.GetMediaType(m.fileName)
}
// HasFileType returns true if this media file is of a given type.
func (m MediaFile) HasFileType(t fs.FileType) bool {
if t == fs.TypeJpeg {
return m.IsJpeg()
}
return m.Type() == t
return m.FileType() == t
}
// IsRaw returns true if this media file a RAW file.
func (m MediaFile) IsRaw() bool {
return m.HasType(fs.TypeRaw)
return m.HasFileType(fs.TypeRaw)
}
// IsPng returns true if this media file a PNG file.
func (m MediaFile) IsPng() bool {
return m.HasType(fs.TypePng)
return m.HasFileType(fs.TypePng)
}
// IsTiff returns true if this media file a TIFF file.
func (m MediaFile) IsTiff() bool {
return m.HasType(fs.TypeTiff)
return m.HasFileType(fs.TypeTiff)
}
// IsImageOther returns true this media file a PNG, GIF, BMP or TIFF file.
func (m MediaFile) IsImageOther() bool {
switch m.Type() {
switch m.FileType() {
case fs.TypeBitmap:
return true
case fs.TypeGif:
@ -535,44 +540,22 @@ func (m MediaFile) IsImageOther() bool {
// IsHEIF returns true if this media file is a High Efficiency Image File Format file.
func (m MediaFile) IsHEIF() bool {
return m.HasType(fs.TypeHEIF)
return m.HasFileType(fs.TypeHEIF)
}
// IsXMP returns true if this file is a XMP sidecar file.
func (m MediaFile) IsXMP() bool {
return m.Type() == fs.TypeXMP
return m.FileType() == fs.TypeXMP
}
// IsSidecar returns true if this media file is a sidecar file (containing metadata).
func (m MediaFile) IsSidecar() bool {
switch m.Type() {
case fs.TypeXMP:
return true
case fs.TypeAAE:
return true
case fs.TypeXML:
return true
case fs.TypeYaml:
return true
case fs.TypeJson:
return true
case fs.TypeText:
return true
case fs.TypeMarkdown:
return true
default:
return false
}
return m.MediaType() == fs.MediaSidecar
}
// IsVideo returns true if this media file is a video file.
func (m MediaFile) IsVideo() bool {
switch m.Type() {
case fs.TypeMovie:
return true
}
return false
return m.MediaType() == fs.MediaVideo
}
// IsPhoto checks if this media file is a photo / image.

View file

@ -611,21 +611,21 @@ func TestMediaFile_HasType(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
assert.Nil(t, err)
assert.Equal(t, false, mediaFile.HasType("jpg"))
assert.Equal(t, false, mediaFile.HasFileType("jpg"))
})
t.Run("/iphone_7.heic", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic")
assert.Nil(t, err)
assert.Equal(t, true, mediaFile.HasType("heif"))
assert.Equal(t, true, mediaFile.HasFileType("heif"))
})
t.Run("/iphone_7.xmp", func(t *testing.T) {
conf := config.TestConfig()
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.xmp")
assert.Nil(t, err)
assert.Equal(t, true, mediaFile.HasType("xmp"))
assert.Equal(t, true, mediaFile.HasFileType("xmp"))
})
}

View file

@ -0,0 +1,76 @@
package remote
import (
"errors"
"net/url"
"strings"
"github.com/photoprism/photoprism/pkg/txt"
)
type Account struct {
AccName string
AccURL string
AccType string
AccKey string
AccUser string
AccPass string
}
func Discover(rawUrl, user, pass string) (result Account, err error) {
if rawUrl == "" {
return result, errors.New("service URL is empty")
}
u, err := url.Parse(rawUrl)
if err != nil {
return result, err
}
u.Host = strings.ToLower(u.Host)
result.AccUser = u.User.Username()
result.AccPass, _ = u.User.Password()
// Extract user info
if user != "" {
result.AccUser = user
}
if pass != "" {
result.AccPass = pass
}
if user != "" || pass != "" {
u.User = url.UserPassword(result.AccUser, result.AccPass)
}
// Set default scheme
if u.Scheme == "" {
u.Scheme = "https"
}
for _, h := range Heuristics {
if !h.MatchDomain(u.Host) {
continue
}
if serviceUrl := h.Discover(u.String(), result.AccUser); serviceUrl != nil {
serviceUrl.User = nil
if w := txt.Keywords(serviceUrl.Host); len(w) > 0 {
result.AccName = strings.Title(w[0])
} else {
result.AccName = serviceUrl.Host
}
result.AccType = h.ServiceType
result.AccURL = serviceUrl.String()
return result, nil
}
}
return result, errors.New("could not connect")
}

View file

@ -35,20 +35,6 @@ func TestDiscover(t *testing.T) {
assert.Equal(t, "photoprism", r.AccPass)
})
t.Run("https", func(t *testing.T) {
r, err := Discover("https://dl.photoprism.org/fixtures/testdata/import/", "", "")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, "Photoprism", r.AccName)
assert.Equal(t, "web", r.AccType)
assert.Equal(t, "https://dl.photoprism.org/fixtures/testdata/import/", r.AccURL)
assert.Equal(t, "", r.AccUser)
assert.Equal(t, "", r.AccPass)
})
t.Run("facebook", func(t *testing.T) {
r, err := Discover("https://www.facebook.com/ob.boris.palmer", "", "")

View file

@ -0,0 +1,64 @@
package remote
import (
"net/url"
"strings"
)
type Heuristic struct {
ServiceType string
Domains []string
Paths []string
Method string
}
var Heuristics = []Heuristic{
{ServiceFacebook, []string{"facebook.com", "www.facebook.com"}, []string{}, "GET"},
{ServiceTwitter, []string{"twitter.com"}, []string{}, "GET"},
{ServiceFlickr, []string{"flickr.com", "www.flickr.com"}, []string{}, "GET"},
{ServiceInstagram, []string{"instagram.com", "www.instagram.com"}, []string{}, "GET"},
{ServiceEyeEm, []string{"eyeem.com", "www.eyeem.com"}, []string{}, "GET"},
{ServiceTelegram, []string{"web.telegram.org", "www.telegram.org", "telegram.org"}, []string{}, "GET"},
{ServiceWhatsApp, []string{"web.whatsapp.com", "www.whatsapp.com", "whatsapp.com"}, []string{}, "GET"},
{ServiceOneDrive, []string{"onedrive.live.com"}, []string{}, "GET"},
{ServiceGDrive, []string{"drive.google.com"}, []string{}, "GET"},
{ServiceGPhotos, []string{"photos.google.com"}, []string{}, "GET"},
{ServiceWebDAV, []string{}, []string{"/", "/webdav", "/remote.php/dav/files/{user}", "/remote.php/webdav", "/dav/files/{user}", "/servlet/webdav.infostore/"}, "PROPFIND"},
}
func (h Heuristic) MatchDomain(match string) bool {
if len(h.Domains) == 0 {
return true
}
for _, m := range h.Domains {
if m == match {
return true
}
}
return false
}
func (h Heuristic) Discover(rawUrl, user string) *url.URL {
u, err := url.Parse(rawUrl)
if err != nil {
return nil
}
if HttpOk(h.Method, u.String()) {
return u
}
for _, p := range h.Paths {
strings.Replace(p, "{user}", user, -1)
u.Path = p
if HttpOk(h.Method, u.String()) {
return u
}
}
return nil
}

44
internal/remote/remote.go Normal file
View file

@ -0,0 +1,44 @@
/*
Package remote implements a remote service abstraction.
Additional information can be found in our Developer Guide:
https://github.com/photoprism/photoprism/wiki
*/
package remote
import (
"net/http"
)
var client = &http.Client{}
const (
ServiceWebDAV = "webdav"
ServiceFacebook = "facebook"
ServiceTwitter = "twitter"
ServiceFlickr = "flickr"
ServiceInstagram = "instagram"
ServiceEyeEm = "eyeem"
ServiceTelegram = "telegram"
ServiceWhatsApp = "whatsapp"
ServiceGPhotos = "gphotos"
ServiceGDrive = "gdrive"
ServiceOneDrive = "onedrive"
)
func HttpOk(method, rawUrl string) bool {
req, err := http.NewRequest(method, rawUrl, nil)
if err != nil {
return false
}
if resp, err := client.Do(req); err != nil {
return false
} else if resp.StatusCode < 400 {
return true
}
return false
}

View file

@ -1,176 +0,0 @@
/*
Package service implements a remote service abstraction.
Additional information can be found in our Developer Guide:
https://github.com/photoprism/photoprism/wiki
*/
package remote
import (
"errors"
"net/http"
"net/url"
"strings"
"github.com/photoprism/photoprism/pkg/txt"
)
var client = &http.Client{}
const (
ServiceWeb = "web"
ServiceWebDAV = "webdav"
ServiceFacebook = "facebook"
ServiceTwitter = "twitter"
ServiceFlickr = "flickr"
ServiceInstagram = "instagram"
ServiceEyeEm = "eyeem"
ServiceTelegram = "telegram"
ServiceWhatsApp = "whatsapp"
ServiceGPhotos = "gphotos"
ServiceGDrive = "gdrive"
ServiceOneDrive = "onedrive"
)
type Account struct {
AccName string
AccURL string
AccType string
AccKey string
AccUser string
AccPass string
}
type Heuristic struct {
ServiceType string
Domains []string
Paths []string
Method string
}
var Heuristics = []Heuristic{
{ServiceFacebook, []string{"facebook.com", "www.facebook.com"}, []string{}, "GET"},
{ServiceTwitter, []string{"twitter.com"}, []string{}, "GET"},
{ServiceFlickr, []string{"flickr.com", "www.flickr.com"}, []string{}, "GET"},
{ServiceInstagram, []string{"instagram.com", "www.instagram.com"}, []string{}, "GET"},
{ServiceEyeEm, []string{"eyeem.com", "www.eyeem.com"}, []string{}, "GET"},
{ServiceTelegram, []string{"web.telegram.org", "www.telegram.org", "telegram.org"}, []string{}, "GET"},
{ServiceWhatsApp, []string{"web.whatsapp.com", "www.whatsapp.com", "whatsapp.com"}, []string{}, "GET"},
{ServiceOneDrive, []string{"onedrive.live.com"}, []string{}, "GET"},
{ServiceGDrive, []string{"drive.google.com"}, []string{}, "GET"},
{ServiceGPhotos, []string{"photos.google.com"}, []string{}, "GET"},
{ServiceWebDAV, []string{}, []string{"/", "/webdav", "/remote.php/dav/files/{user}", "/remote.php/webdav", "/dav/files/{user}", "/servlet/webdav.infostore/"}, "PROPFIND"},
{ServiceWeb, []string{}, []string{}, "GET"},
}
func HttpOk(method, rawUrl string) bool {
req, err := http.NewRequest(method, rawUrl, nil)
if err != nil {
return false
}
if resp, err := client.Do(req); err != nil {
return false
} else if resp.StatusCode < 400 {
return true
}
return false
}
func (h Heuristic) MatchDomain(match string) bool {
if len(h.Domains) == 0 {
return true
}
for _, m := range h.Domains {
if m == match {
return true
}
}
return false
}
func (h Heuristic) Discover(rawUrl, user string) *url.URL {
u, err := url.Parse(rawUrl)
if err != nil {
return nil
}
if HttpOk(h.Method, u.String()) {
return u
}
for _, p := range h.Paths {
strings.Replace(p, "{user}", user, -1)
u.Path = p
if HttpOk(h.Method, u.String()) {
return u
}
}
return nil
}
func Discover(rawUrl, user, pass string) (result Account, err error) {
if rawUrl == "" {
return result, errors.New("service URL is empty")
}
u, err := url.Parse(rawUrl)
if err != nil {
return result, err
}
u.Host = strings.ToLower(u.Host)
result.AccUser = u.User.Username()
result.AccPass, _ = u.User.Password()
// Extract user info
if user != "" {
result.AccUser = user
}
if pass != "" {
result.AccPass = pass
}
if user != "" || pass != "" {
u.User = url.UserPassword(result.AccUser, result.AccPass)
}
// Set default scheme
if u.Scheme == "" {
u.Scheme = "https"
}
for _, h := range Heuristics {
if !h.MatchDomain(u.Host) {
continue
}
if serviceUrl := h.Discover(u.String(), result.AccUser); serviceUrl != nil {
serviceUrl.User = nil
if w := txt.Keywords(serviceUrl.Host); len(w) > 0 {
result.AccName = strings.Title(w[0])
} else {
result.AccName = serviceUrl.Host
}
result.AccType = h.ServiceType
result.AccURL = serviceUrl.String()
return result, nil
}
}
return result, errors.New("could not connect")
}

View file

@ -14,7 +14,7 @@ import (
"github.com/disintegration/imaging"
)
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.Type) {
func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.FileType) {
method = ResampleFit
filter = imaging.Lanczos
format = fs.TypeJpeg

View file

@ -166,10 +166,31 @@ func (s *Sync) getRemoteFiles(a entity.Account) (complete bool, err error) {
}
f := entity.NewFileSync(a.ID, file.Abs)
f.Status = entity.FileSyncIgnore
f.RemoteDate = file.Date
f.RemoteSize = file.Size
// Select supported types for download
mediaType := fs.GetMediaType(file.Name)
switch mediaType {
case fs.MediaImage:
f.Status = entity.FileSyncNew
case fs.MediaSidecar:
f.Status = entity.FileSyncNew
case fs.MediaRaw:
if a.SyncRaw {
f.Status = entity.FileSyncNew
}
}
f.FirstOrCreate(db)
if f.Status == entity.FileSyncIgnore && mediaType == fs.MediaRaw && a.SyncRaw {
f.Status = entity.FileSyncNew
db.Save(&f)
}
if f.Status == entity.FileSyncDownloaded && !f.RemoteDate.Equal(file.Date) {
f.Status = entity.FileSyncNew
f.RemoteDate = file.Date

109
pkg/fs/filetype.go Normal file
View file

@ -0,0 +1,109 @@
package fs
import (
_ "image/gif" // Import for image.
_ "image/jpeg"
_ "image/png"
"path/filepath"
"strings"
)
type FileType string
const (
TypeJpeg FileType = "jpg" // JPEG image file.
TypePng FileType = "png" // PNG image file.
TypeGif FileType = "gif" // GIF image file.
TypeTiff FileType = "tiff" // TIFF image file.
TypeBitmap FileType = "bmp" // BMP image file.
TypeRaw FileType = "raw" // RAW image file.
TypeHEIF FileType = "heif" // High Efficiency Image File Format
TypeMov FileType = "mov" // Video files.
TypeMP4 FileType = "mp4"
TypeAvi FileType = "avi"
TypeXMP FileType = "xmp" // Adobe XMP sidecar file (XML).
TypeAAE FileType = "aae" // Apple sidecar file (XML).
TypeXML FileType = "xml" // XML metadata / config / sidecar file.
TypeYaml FileType = "yml" // YAML metadata / config / sidecar file.
TypeToml FileType = "toml" // Tom's Obvious, Minimal Language sidecar file.
TypeJson FileType = "json" // JSON metadata / config / sidecar file.
TypeText FileType = "txt" // Text config / sidecar file.
TypeMarkdown FileType = "md" // Markdown text sidecar file.
TypeOther FileType = "unknown" // Unknown file format.
)
// FileExt contains the filename extensions of file formats known to PhotoPrism.
var FileExt = map[string]FileType{
".bmp": TypeBitmap,
".gif": TypeGif,
".tif": TypeTiff,
".tiff": TypeTiff,
".png": TypePng,
".crw": TypeRaw,
".cr2": TypeRaw,
".nef": TypeRaw,
".arw": TypeRaw,
".dng": TypeRaw,
".mov": TypeMov,
".avi": TypeAvi,
".mp4": TypeMP4,
".yml": TypeYaml,
".jpg": TypeJpeg,
".thm": TypeJpeg,
".jpeg": TypeJpeg,
".xmp": TypeXMP,
".aae": TypeAAE,
".heif": TypeHEIF,
".heic": TypeHEIF,
".3fr": TypeRaw,
".ari": TypeRaw,
".bay": TypeRaw,
".cr3": TypeRaw,
".cap": TypeRaw,
".data": TypeRaw,
".dcs": TypeRaw,
".dcr": TypeRaw,
".drf": TypeRaw,
".eip": TypeRaw,
".erf": TypeRaw,
".fff": TypeRaw,
".gpr": TypeRaw,
".iiq": TypeRaw,
".k25": TypeRaw,
".kdc": TypeRaw,
".mdc": TypeRaw,
".mef": TypeRaw,
".mos": TypeRaw,
".mrw": TypeRaw,
".nrw": TypeRaw,
".obm": TypeRaw,
".orf": TypeRaw,
".pef": TypeRaw,
".ptx": TypeRaw,
".pxn": TypeRaw,
".r3d": TypeRaw,
".raf": TypeRaw,
".raw": TypeRaw,
".rwl": TypeRaw,
".rw2": TypeRaw,
".rwz": TypeRaw,
".sr2": TypeRaw,
".srf": TypeRaw,
".srw": TypeRaw,
".x3f": TypeRaw,
".xml": TypeXML,
".txt": TypeText,
".md": TypeMarkdown,
".json": TypeJson,
}
func GetFileType(fileName string) FileType {
fileExt := strings.ToLower(filepath.Ext(fileName))
result, ok := FileExt[fileExt]
if !ok {
result = TypeOther
}
return result
}

24
pkg/fs/filetype_test.go Normal file
View file

@ -0,0 +1,24 @@
package fs
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetFileType(t *testing.T) {
t.Run("jpeg", func(t *testing.T) {
result := GetFileType("testdata/test.jpg")
assert.Equal(t, TypeJpeg, result)
})
t.Run("raw", func(t *testing.T) {
result := GetFileType("testdata/test (jpg).CR2")
assert.Equal(t, TypeRaw, result)
})
t.Run("empty", func(t *testing.T) {
result := GetFileType("")
assert.Equal(t, TypeOther, result)
})
}

43
pkg/fs/mediatype.go Normal file
View file

@ -0,0 +1,43 @@
package fs
type MediaType string
const (
MediaRaw MediaType = "raw"
MediaImage MediaType = "image"
MediaVideo MediaType = "video"
MediaSidecar MediaType = "sidecar"
MediaOther MediaType = "other"
)
var MediaTypes = map[FileType]MediaType{
TypeRaw: MediaRaw,
TypeJpeg: MediaImage,
TypePng: MediaImage,
TypeGif: MediaImage,
TypeTiff: MediaImage,
TypeBitmap: MediaImage,
TypeHEIF: MediaImage,
TypeAvi: MediaVideo,
TypeMP4: MediaVideo,
TypeMov: MediaVideo,
TypeXMP: MediaSidecar,
TypeXML: MediaSidecar,
TypeAAE: MediaSidecar,
TypeYaml: MediaSidecar,
TypeText: MediaSidecar,
TypeJson: MediaSidecar,
TypeToml: MediaSidecar,
TypeMarkdown: MediaSidecar,
TypeOther: MediaOther,
}
func GetMediaType(fileName string) MediaType {
result, ok := MediaTypes[GetFileType(fileName)]
if !ok {
result = MediaOther
}
return result
}

29
pkg/fs/mediatype_test.go Normal file
View file

@ -0,0 +1,29 @@
package fs
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGetMediaType(t *testing.T) {
t.Run("jpeg", func(t *testing.T) {
result := GetMediaType("testdata/test.jpg")
assert.Equal(t, MediaImage, result)
})
t.Run("raw", func(t *testing.T) {
result := GetMediaType("testdata/test (jpg).CR2")
assert.Equal(t, MediaRaw, result)
})
t.Run("sidecar", func(t *testing.T) {
result := GetMediaType("/IMG_4120.AAE")
assert.Equal(t, MediaSidecar, result)
})
t.Run("empty", func(t *testing.T) {
result := GetMediaType("")
assert.Equal(t, MediaOther, result)
})
}

View file

@ -5,6 +5,10 @@ import (
"os"
)
const (
MimeTypeJpeg = "image/jpeg"
)
// MimeType returns the mime type of a file, empty string if unknown.
func MimeType(filename string) string {
handle, err := os.Open(filename)

View file

@ -1,112 +0,0 @@
package fs
import (
_ "image/gif" // Import for image.
_ "image/jpeg"
_ "image/png"
)
type Type string
const (
// JPEG image file.
TypeJpeg Type = "jpg"
// PNG image file.
TypePng Type = "png"
// GIF image file.
TypeGif Type = "gif"
// TIFF image file.
TypeTiff Type = "tiff"
// BMP image file.
TypeBitmap Type = "bmp"
// RAW image file.
TypeRaw Type = "raw"
// High Efficiency Image File Format.
TypeHEIF Type = "heif" // High Efficiency Image File Format
// Movie file.
TypeMovie Type = "mov"
// Adobe XMP sidecar file (XML).
TypeXMP Type = "xmp"
// Apple sidecar file (XML).
TypeAAE Type = "aae"
// XML metadata / config / sidecar file.
TypeXML Type = "xml"
// YAML metadata / config / sidecar file.
TypeYaml Type = "yml"
// JSON metadata / config / sidecar file.
TypeJson Type = "json"
// Text config / sidecar file.
TypeText Type = "txt"
// Markdown text sidecar file.
TypeMarkdown Type = "md"
// Unknown file format.
TypeOther Type = "unknown"
)
const (
MimeTypeJpeg = "image/jpeg"
)
// Ext contains the filename extensions of file formats known to PhotoPrism.
var Ext = map[string]Type{
".bmp": TypeBitmap,
".gif": TypeGif,
".tif": TypeTiff,
".tiff": TypeTiff,
".png": TypePng,
".crw": TypeRaw,
".cr2": TypeRaw,
".nef": TypeRaw,
".arw": TypeRaw,
".dng": TypeRaw,
".mov": TypeMovie,
".avi": TypeMovie,
".yml": TypeYaml,
".jpg": TypeJpeg,
".thm": TypeJpeg,
".jpeg": TypeJpeg,
".xmp": TypeXMP,
".aae": TypeAAE,
".heif": TypeHEIF,
".heic": TypeHEIF,
".3fr": TypeRaw,
".ari": TypeRaw,
".bay": TypeRaw,
".cr3": TypeRaw,
".cap": TypeRaw,
".data": TypeRaw,
".dcs": TypeRaw,
".dcr": TypeRaw,
".drf": TypeRaw,
".eip": TypeRaw,
".erf": TypeRaw,
".fff": TypeRaw,
".gpr": TypeRaw,
".iiq": TypeRaw,
".k25": TypeRaw,
".kdc": TypeRaw,
".mdc": TypeRaw,
".mef": TypeRaw,
".mos": TypeRaw,
".mrw": TypeRaw,
".nrw": TypeRaw,
".obm": TypeRaw,
".orf": TypeRaw,
".pef": TypeRaw,
".ptx": TypeRaw,
".pxn": TypeRaw,
".r3d": TypeRaw,
".raf": TypeRaw,
".raw": TypeRaw,
".rwl": TypeRaw,
".rw2": TypeRaw,
".rwz": TypeRaw,
".sr2": TypeRaw,
".srf": TypeRaw,
".srw": TypeRaw,
".x3f": TypeRaw,
".xml": TypeXML,
".txt": TypeText,
".md": TypeMarkdown,
".json": TypeJson,
}