Sync: Ignore unsupported file types #225
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
parent
924eeac55c
commit
b020b4e415
27 changed files with 461 additions and 387 deletions
|
@ -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);
|
||||
|
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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);"`
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
76
internal/remote/discover.go
Normal file
76
internal/remote/discover.go
Normal 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")
|
||||
}
|
|
@ -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", "", "")
|
||||
|
64
internal/remote/heuristic.go
Normal file
64
internal/remote/heuristic.go
Normal 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
44
internal/remote/remote.go
Normal 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
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
109
pkg/fs/filetype.go
Normal 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
24
pkg/fs/filetype_test.go
Normal 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
43
pkg/fs/mediatype.go
Normal 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
29
pkg/fs/mediatype_test.go
Normal 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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
|
|
112
pkg/fs/types.go
112
pkg/fs/types.go
|
@ -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,
|
||||
}
|
Loading…
Reference in a new issue