diff --git a/docker/develop/bookworm-slim/Dockerfile b/docker/develop/bookworm-slim/Dockerfile
index 8d63d76cf..d252c1f59 100644
--- a/docker/develop/bookworm-slim/Dockerfile
+++ b/docker/develop/bookworm-slim/Dockerfile
@@ -31,30 +31,13 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
useradd -m -g 1000 -u 1000 -d /photoprism -G video,render photoprism && \
chmod 777 /photoprism && \
apt-get update && apt-get -qq dist-upgrade && apt-get -qq install --no-install-recommends \
- ca-certificates \
- jq \
- zip \
- gpg \
- lshw \
- wget \
- curl \
- make \
- sudo \
- bash \
- sqlite3 \
- tzdata \
- libc6 \
- libatomic1 \
- libheif-examples \
- librsvg2-bin \
- exiftool \
- darktable \
- rawtherapee \
- ffmpeg \
- ffmpegthumbnailer \
- libavcodec-extra \
- mariadb-client \
- && \
+ libc6 ca-certificates sudo bash tzdata \
+ gpg zip unzip wget curl rsync make nano \
+ jq lsof lshw sqlite3 mariadb-client \
+ exiftool darktable rawtherapee libheif-examples librsvg2-bin \
+ ffmpeg ffmpegthumbnailer libavcodec-extra libwebm1 \
+ libmatroska7 libdvdread8 libebml5 libgav1-0 libatomic1 \
+ libx264-163 libx265-199 && \
install -d -m 0777 -o 1000 -g 1000 \
/var/lib/photoprism \
/tmp/photoprism \
diff --git a/docker/develop/bookworm/Dockerfile b/docker/develop/bookworm/Dockerfile
index f424c642a..0224b7110 100644
--- a/docker/develop/bookworm/Dockerfile
+++ b/docker/develop/bookworm/Dockerfile
@@ -38,60 +38,20 @@ RUN echo 'APT::Acquire::Retries "3";' > /etc/apt/apt.conf.d/80retries && \
useradd -m -g 1000 -u 1000 -d /photoprism -G video,render photoprism && \
chmod 777 /photoprism && \
apt-get update && apt-get -qq dist-upgrade && apt-get -qq install --no-install-recommends \
- apt-utils \
- gpg \
- pkg-config \
- software-properties-common \
- ca-certificates \
- build-essential \
- gcc \
- g++ \
- sudo \
- bash \
- make \
- nano \
- lsof \
- lshw \
- wget \
- curl \
- rsync \
- jq \
- git \
- zip \
- unzip \
- gettext \
- chromium \
- chromium-driver \
- chromium-sandbox \
- firefox-esr \
- sqlite3 \
- libc6-dev \
- libssl-dev \
- libxft-dev \
- libhdf5-serial-dev \
- libpng-dev \
- libheif-examples \
- librsvg2-bin \
- libzmq3-dev \
- libx264-dev \
- libx265-dev \
- libnss3 \
- libfreetype6 \
- libfreetype6-dev \
- libfontconfig1 \
- libfontconfig1-dev \
- fonts-roboto \
- tzdata \
- exiftool \
- rawtherapee \
- ffmpeg \
- darktable \
- ffmpegthumbnailer \
- libavcodec-extra \
- davfs2 \
- chrpath \
- apache2-utils \
- mariadb-client \
+ libc6 ca-certificates sudo bash tzdata \
+ gpg zip unzip wget curl rsync make nano \
+ jq lsof lshw sqlite3 mariadb-client \
+ exiftool darktable rawtherapee libheif-examples librsvg2-bin \
+ ffmpeg ffmpegthumbnailer libavcodec-extra libwebm1 \
+ libmatroska7 libdvdread8 libebml5 libgav1-0 libatomic1 \
+ libx264-163 libx265-199 && \
+ apt-get -qq install --no-install-recommends \
+ apt-utils pkg-config software-properties-common \
+ build-essential gcc g++ git gettext davfs2 chrpath apache2-utils \
+ chromium chromium-driver chromium-sandbox firefox-esr \
+ libx264-dev libx265-dev libpng-dev libxft-dev \
+ libc6-dev libhdf5-serial-dev libzmq3-dev libssl-dev libnss3 \
+ libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev fonts-roboto \
&& \
/scripts/install-nodejs.sh && \
/scripts/install-tensorflow.sh && \
diff --git a/frontend/src/common/util.js b/frontend/src/common/util.js
index 02e240e62..a3252199c 100644
--- a/frontend/src/common/util.js
+++ b/frontend/src/common/util.js
@@ -145,25 +145,100 @@ export default class Util {
start = now;
}
+ static capitalize(s) {
+ if (!s || s === "") {
+ return "";
+ }
+
+ return s.replace(/\w\S*/g, (w) => w.replace(/^\w/, (c) => c.toUpperCase()));
+ }
+
+ static fileType(value) {
+ if (!value || typeof value !== "string") {
+ return "";
+ }
+
+ switch (value) {
+ case "raw":
+ return "Unprocessed Sensor Data (RAW)";
+ case "mov":
+ case "qt":
+ return "Apple QuickTime";
+ case "bmp":
+ return "Bitmap";
+ case "png":
+ return "Portable Network Graphics";
+ case "tiff":
+ return "TIFF";
+ case "gif":
+ return "GIF";
+ case "avc":
+ case "avc1":
+ return "Advanced Video Coding (AVC) / H.264";
+ case "hevc":
+ case "hvc":
+ case "hvc1":
+ return "High Efficiency Video Coding (HEVC) / H.265";
+ case "mkv":
+ return "Matroska Multimedia Container";
+ case "webp":
+ return "Google WebP";
+ case "webm":
+ return "Google WebM";
+ case "flv":
+ return "Flash";
+ case "mpg":
+ return "MPEG";
+ case "mjpg":
+ return "Motion JPEG";
+ case "ogg":
+ case "ogv":
+ return "Ogg Media";
+ case "wmv":
+ return "Windows Media";
+ default:
+ return value.toUpperCase();
+ }
+ }
+
static codecName(value) {
if (!value || typeof value !== "string") {
return "";
}
switch (value) {
+ case "raw":
+ return "Unprocessed Sensor Data (RAW)";
+ case "mov":
+ case "qt":
+ return "Apple QuickTime (MOV)";
+ case "avc":
case "avc1":
return "Advanced Video Coding (AVC) / H.264";
+ case "hevc":
+ case "hvc":
case "hvc1":
return "High Efficiency Video Coding (HEVC) / H.265";
+ case "vvc":
+ return "Versatile Video Coding (VVC) / H.266";
case "av01":
return "AOMedia Video 1 (AV1)";
+ case "gif":
+ return "Graphics Interchange Format (GIF)";
+ case "mkv":
+ return "Matroska Multimedia Container (MKV)";
+ case "webp":
+ return "Google WebP";
+ case "webm":
+ return "Google WebM";
case "mpeg":
return "Moving Picture Experts Group (MPEG)";
case "mjpg":
return "Motion JPEG (M-JPEG)";
case "heif":
- case "heic":
return "High Efficiency Image File Format (HEIF)";
+ case "heic":
+ return "High Efficiency Image Container (HEIC)";
case "1":
return "Uncompressed";
case "2":
diff --git a/frontend/src/dialog/photo/files.vue b/frontend/src/dialog/photo/files.vue
index 5df3da551..cfe8874de 100644
--- a/frontend/src/dialog/photo/files.vue
+++ b/frontend/src/dialog/photo/files.vue
@@ -82,18 +82,18 @@
Frames
@@ -278,6 +284,20 @@ export default {
},
computed: {},
methods: {
+ formatDuration(file) {
+ if (!file || !file.Duration) {
+ return "";
+ }
+
+ return Util.duration(file.Duration);
+ },
+ fileType(file) {
+ if (!file || !file.FileType) {
+ return "";
+ }
+
+ return Util.fileType(file.FileType);
+ },
codecName(file) {
if (!file || !file.Codec) {
return "";
diff --git a/frontend/src/model/file.js b/frontend/src/model/file.js
index 37001778a..e291e7c17 100644
--- a/frontend/src/model/file.js
+++ b/frontend/src/model/file.js
@@ -30,7 +30,7 @@ import Util from "common/util";
import { config } from "app/session";
import { $gettext } from "common/vm";
import download from "common/download";
-import { MediaAnimated } from "./photo";
+import { MediaImage } from "./photo";
export class File extends RestModel {
getDefaults() {
@@ -38,6 +38,9 @@ export class File extends RestModel {
UID: "",
PhotoUID: "",
InstanceID: "",
+ MediaID: "",
+ MediaUTC: 0,
+ TakenAt: "",
Root: "/",
Name: "",
OriginalName: "",
@@ -184,18 +187,54 @@ export class File extends RestModel {
return info.join(", ");
}
+ storageInfo() {
+ if (!this.Root || this.Root === "") {
+ return "";
+ }
+
+ if (this.Root.length === 1) {
+ return $gettext("Originals");
+ } else {
+ return Util.capitalize(this.Root);
+ }
+ }
+
typeInfo() {
- if (this.Type === MediaAnimated) {
- return $gettext("Animation");
- }
+ let info = [];
- if (this.Video) {
- return $gettext("Video");
+ if (
+ this.MediaType &&
+ this.Frames &&
+ this.MediaType === MediaImage &&
+ this.Frames &&
+ this.Frames > 0
+ ) {
+ info.push($gettext("Animated"));
} else if (this.Sidecar) {
- return $gettext("Sidecar");
+ info.push($gettext("Sidecar"));
}
- return this.FileType.toUpperCase();
+ if (this.Primary && !this.MediaType) {
+ info.push($gettext("Image"));
+ return info.join(" ");
+ } else if (this.Video && !this.MediaType) {
+ info.push($gettext("Video"));
+ return info.join(" ");
+ } else {
+ const format = Util.fileType(this.FileType);
+ if (format) {
+ info.push(format);
+ }
+
+ if (this.MediaType && this.MediaType !== this.FileType) {
+ const media = Util.capitalize(this.MediaType);
+ if (media) {
+ info.push(media);
+ }
+ }
+
+ return info.join(" ");
+ }
}
sizeInfo() {
diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js
index ed66b606f..8cf3563ad 100644
--- a/frontend/src/model/photo.js
+++ b/frontend/src/model/photo.js
@@ -43,6 +43,7 @@ export const FormatGif = "gif";
export const FormatJpeg = "jpg";
export const MediaImage = "image";
export const MediaAnimated = "animated";
+export const MediaSidecar = "sidecar";
export const MediaVideo = "video";
export const MediaLive = "live";
export const MediaRaw = "raw";
@@ -535,9 +536,10 @@ export class Photo extends RestModel {
}
downloadAll() {
- const settings = config.settings();
+ const s = config.settings();
- if (!settings || !settings.features || !settings.download || !settings.features.download) {
+ if (!s || !s.features || !s.download || !s.features.download || s.download.disabled) {
+ console.log("download: disabled in settings", s.features, s.download);
return;
}
@@ -560,23 +562,30 @@ export class Photo extends RestModel {
return;
}
- // Skip sidecar files.
- if (file.Sidecar) {
+ // Originals only?
+ if (s.download.originals && file.Root.length > 1) {
// Don't download broken files and sidecars.
- if (config.debug) console.log("download: skipped sidecar", file);
+ if (config.debug) console.log(`download: skipped ${file.Root} file ${file.Name}`);
return;
}
- // Skip RAW images.
- if (!settings.download.raw && file.FileType === MediaRaw) {
- if (config.debug) console.log("download: skipped raw", file);
+ // Skip metadata sidecar files?
+ if (!s.download.mediaSidecar && (file.MediaType === MediaSidecar || file.Sidecar)) {
+ // Don't download broken files and sidecars.
+ if (config.debug) console.log(`download: skipped sidecar file ${file.Name}`);
return;
}
- // Skip related images if video.
+ // Skip RAW images?
+ if (!s.download.mediaRaw && (file.MediaType === MediaRaw || file.FileType === MediaRaw)) {
+ if (config.debug) console.log(`download: skipped raw file ${file.Name}`);
+ return;
+ }
+
+ // If this is a video, always skip stacked images...
// see https://github.com/photoprism/photoprism/issues/1436
- if (this.Type === MediaVideo && !file.Video) {
- if (config.debug) console.log("download: skipped image", file);
+ if (this.Type === MediaVideo && !(file.MediaType === MediaVideo || file.Video)) {
+ if (config.debug) console.log(`download: skipped video sidecar ${file.Name}`);
return;
}
diff --git a/frontend/tests/unit/model/file_test.js b/frontend/tests/unit/model/file_test.js
index 206752fe5..ccc4f729e 100644
--- a/frontend/tests/unit/model/file_test.js
+++ b/frontend/tests/unit/model/file_test.js
@@ -236,7 +236,21 @@ describe("model/file", () => {
UpdatedAt: "2012-07-08T14:45:39Z",
};
const file3 = new File(values3);
- assert.equal(file3.typeInfo(), "Sidecar");
+ assert.equal(file3.typeInfo(), "Sidecar JPG");
+ const values4 = {
+ InstanceID: 5,
+ UID: "ABC123",
+ Hash: "54ghtfd",
+ FileType: "gif",
+ MediaType: "image",
+ Duration: 8009,
+ Name: "1/2/IMG123.jpg",
+ Sidecar: true,
+ CreatedAt: "2012-07-08T14:45:39Z",
+ UpdatedAt: "2012-07-08T14:45:39Z",
+ };
+ const file4 = new File(values4);
+ assert.equal(file4.typeInfo(), "Sidecar GIF Image");
});
it("should get size info", () => {
diff --git a/internal/api/account.go b/internal/api/account.go
index ae3285ee9..948c96fd2 100644
--- a/internal/api/account.go
+++ b/internal/api/account.go
@@ -18,8 +18,8 @@ import (
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/workers"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Namespaces for caching and logs.
@@ -49,7 +49,7 @@ func GetAccount(router *gin.RouterGroup) {
return
}
- id := sanitize.IdUint(c.Param("id"))
+ id := clean.IdUint(c.Param("id"))
if m, err := query.AccountByID(id); err == nil {
c.JSON(http.StatusOK, m)
@@ -82,7 +82,7 @@ func GetAccountFolders(router *gin.RouterGroup) {
}
start := time.Now()
- id := sanitize.IdUint(c.Param("id"))
+ id := clean.IdUint(c.Param("id"))
cache := service.FolderCache()
cacheKey := fmt.Sprintf("%s:%d", accountFolder, id)
@@ -132,7 +132,7 @@ func ShareWithAccount(router *gin.RouterGroup) {
return
}
- id := sanitize.IdUint(c.Param("id"))
+ id := clean.IdUint(c.Param("id"))
m, err := query.AccountByID(id)
@@ -150,13 +150,9 @@ func ShareWithAccount(router *gin.RouterGroup) {
folder := f.Folder
- // Select files to be shared.
- o := query.FileSelection{
- Video: true,
- OriginalsOnly: m.ShareOriginals(),
- PrimaryOnly: !m.ShareOriginals(),
- }
- files, err := query.SelectedFiles(f.Selection, o)
+ // Find files to share.
+ selection := query.ShareSelection(m.ShareOriginals())
+ files, err := query.SelectedFiles(f.Selection, selection)
if err != nil {
AbortEntityNotFound(c)
@@ -252,7 +248,7 @@ func UpdateAccount(router *gin.RouterGroup) {
return
}
- id := sanitize.IdUint(c.Param("id"))
+ id := clean.IdUint(c.Param("id"))
m, err := query.AccountByID(id)
@@ -323,7 +319,7 @@ func DeleteAccount(router *gin.RouterGroup) {
return
}
- id := sanitize.IdUint(c.Param("id"))
+ id := clean.IdUint(c.Param("id"))
m, err := query.AccountByID(id)
diff --git a/internal/api/album.go b/internal/api/album.go
index b39473068..871dfd20b 100644
--- a/internal/api/album.go
+++ b/internal/api/album.go
@@ -16,7 +16,7 @@ import (
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
var albumMutex = sync.Mutex{}
@@ -35,7 +35,7 @@ func SaveAlbumAsYaml(a entity.Album) {
if err := a.SaveAsYaml(fileName); err != nil {
log.Errorf("album: %s (update yaml)", err)
} else {
- log.Debugf("album: updated yaml file %s", sanitize.Log(filepath.Base(fileName)))
+ log.Debugf("album: updated yaml file %s", clean.Log(filepath.Base(fileName)))
}
}
@@ -51,7 +51,7 @@ func GetAlbum(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
a, err := query.AlbumByUID(id)
if err != nil {
@@ -96,7 +96,7 @@ func CreateAlbum(router *gin.RouterGroup) {
// Create new album.
if err := a.Create(); err != nil {
- AbortAlreadyExists(c, sanitize.Log(a.AlbumTitle))
+ AbortAlreadyExists(c, clean.Log(a.AlbumTitle))
return
}
@@ -124,7 +124,7 @@ func UpdateAlbum(router *gin.RouterGroup) {
return
}
- uid := sanitize.IdString(c.Param("uid"))
+ uid := clean.IdString(c.Param("uid"))
a, err := query.AlbumByUID(uid)
if err != nil {
@@ -179,7 +179,7 @@ func DeleteAlbum(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
a, err := query.AlbumByUID(id)
@@ -212,7 +212,7 @@ func DeleteAlbum(router *gin.RouterGroup) {
SaveAlbumAsYaml(a)
- event.SuccessMsg(i18n.MsgAlbumDeleted, sanitize.Log(a.AlbumTitle))
+ event.SuccessMsg(i18n.MsgAlbumDeleted, clean.Log(a.AlbumTitle))
c.JSON(http.StatusOK, a)
})
@@ -233,7 +233,7 @@ func LikeAlbum(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
a, err := query.AlbumByUID(id)
if err != nil {
@@ -271,7 +271,7 @@ func DislikeAlbum(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
a, err := query.AlbumByUID(id)
if err != nil {
@@ -306,7 +306,7 @@ func CloneAlbums(router *gin.RouterGroup) {
return
}
- a, err := query.AlbumByUID(sanitize.IdString(c.Param("uid")))
+ a, err := query.AlbumByUID(clean.IdString(c.Param("uid")))
if err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
@@ -341,7 +341,7 @@ func CloneAlbums(router *gin.RouterGroup) {
}
if len(added) > 0 {
- event.SuccessMsg(i18n.MsgSelectionAddedTo, sanitize.Log(a.Title()))
+ event.SuccessMsg(i18n.MsgSelectionAddedTo, clean.Log(a.Title()))
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
@@ -371,7 +371,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
return
}
- uid := sanitize.IdString(c.Param("uid"))
+ uid := clean.IdString(c.Param("uid"))
a, err := query.AlbumByUID(uid)
if err != nil {
@@ -392,9 +392,9 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
if len(added) > 0 {
if len(added) == 1 {
- event.SuccessMsg(i18n.MsgEntryAddedTo, sanitize.Log(a.Title()))
+ event.SuccessMsg(i18n.MsgEntryAddedTo, clean.Log(a.Title()))
} else {
- event.SuccessMsg(i18n.MsgEntriesAddedTo, len(added), sanitize.Log(a.Title()))
+ event.SuccessMsg(i18n.MsgEntriesAddedTo, len(added), clean.Log(a.Title()))
}
RemoveFromAlbumCoverCache(a.AlbumUID)
@@ -432,7 +432,7 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
return
}
- a, err := query.AlbumByUID(sanitize.IdString(c.Param("uid")))
+ a, err := query.AlbumByUID(clean.IdString(c.Param("uid")))
if err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
@@ -443,9 +443,9 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
if len(removed) > 0 {
if len(removed) == 1 {
- event.SuccessMsg(i18n.MsgEntryRemovedFrom, sanitize.Log(a.Title()))
+ event.SuccessMsg(i18n.MsgEntryRemovedFrom, clean.Log(a.Title()))
} else {
- event.SuccessMsg(i18n.MsgEntriesRemovedFrom, len(removed), sanitize.Log(sanitize.Log(a.Title())))
+ event.SuccessMsg(i18n.MsgEntriesRemovedFrom, len(removed), clean.Log(clean.Log(a.Title())))
}
RemoveFromAlbumCoverCache(a.AlbumUID)
diff --git a/internal/api/api.go b/internal/api/api.go
index dadc55a09..bf49fdb56 100644
--- a/internal/api/api.go
+++ b/internal/api/api.go
@@ -34,7 +34,7 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
var log = event.Log
@@ -60,7 +60,7 @@ func UpdateClientConfig() {
func Abort(c *gin.Context, code int, id i18n.Message, params ...interface{}) {
resp := i18n.NewResponse(code, id, params...)
- log.Debugf("api: abort %s with code %d (%s)", sanitize.Log(c.FullPath()), code, resp.String())
+ log.Debugf("api: abort %s with code %d (%s)", clean.Log(c.FullPath()), code, resp.String())
c.AbortWithStatusJSON(code, resp)
}
@@ -70,7 +70,7 @@ func Error(c *gin.Context, code int, err error, id i18n.Message, params ...inter
if err != nil {
resp.Details = err.Error()
- log.Errorf("api: error %s with code %d in %s (%s)", sanitize.Log(err.Error()), code, sanitize.Log(c.FullPath()), resp.String())
+ log.Errorf("api: error %s with code %d in %s (%s)", clean.Log(err.Error()), code, clean.Log(c.FullPath()), resp.String())
}
c.AbortWithStatusJSON(code, resp)
diff --git a/internal/api/batch.go b/internal/api/batch.go
index 3a2b55876..b199bfe3f 100644
--- a/internal/api/batch.go
+++ b/internal/api/batch.go
@@ -14,7 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// BatchPhotosArchive moves multiple photos to the archive.
@@ -41,7 +41,7 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
return
}
- log.Infof("photos: archiving %s", sanitize.Log(f.String()))
+ log.Infof("photos: archiving %s", clean.Log(f.String()))
if service.Config().BackupYaml() {
// Fetch selection from index.
@@ -105,7 +105,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
return
}
- log.Infof("photos: restoring %s", sanitize.Log(f.String()))
+ log.Infof("photos: restoring %s", clean.Log(f.String()))
if service.Config().BackupYaml() {
// Fetch selection from index.
@@ -168,7 +168,7 @@ func BatchPhotosApprove(router *gin.RouterGroup) {
return
}
- log.Infof("photos: approving %s", sanitize.Log(f.String()))
+ log.Infof("photos: approving %s", clean.Log(f.String()))
// Fetch selection from index.
photos, err := query.SelectedPhotos(f)
@@ -221,7 +221,7 @@ func BatchAlbumsDelete(router *gin.RouterGroup) {
return
}
- log.Infof("albums: deleting %s", sanitize.Log(f.String()))
+ log.Infof("albums: deleting %s", clean.Log(f.String()))
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.Album{})
entity.Db().Where("album_uid IN (?)", f.Albums).Delete(&entity.PhotoAlbum{})
@@ -258,7 +258,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
return
}
- log.Infof("photos: updating private flag for %s", sanitize.Log(f.String()))
+ log.Infof("photos: updating private flag for %s", clean.Log(f.String()))
if err := entity.Db().Model(entity.Photo{}).Where("photo_uid IN (?)", f.Photos).UpdateColumn("photo_private",
gorm.Expr("CASE WHEN photo_private > 0 THEN 0 ELSE 1 END")).Error; err != nil {
@@ -312,7 +312,7 @@ func BatchLabelsDelete(router *gin.RouterGroup) {
return
}
- log.Infof("labels: deleting %s", sanitize.Log(f.String()))
+ log.Infof("labels: deleting %s", clean.Log(f.String()))
var labels entity.Labels
@@ -364,7 +364,7 @@ func BatchPhotosDelete(router *gin.RouterGroup) {
return
}
- log.Infof("photos: deleting %s", sanitize.Log(f.String()))
+ log.Infof("photos: deleting %s", clean.Log(f.String()))
// Fetch selection from index.
photos, err := query.SelectedPhotos(f)
diff --git a/internal/api/config.go b/internal/api/config.go
index 00ed29b10..47d36c31c 100644
--- a/internal/api/config.go
+++ b/internal/api/config.go
@@ -5,7 +5,7 @@ import (
"os"
"path/filepath"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"gopkg.in/yaml.v2"
@@ -87,13 +87,13 @@ func SaveConfigOptions(router *gin.RouterGroup) {
yamlData, err := os.ReadFile(fileName)
if err != nil {
- log.Errorf("config: failed loading values from %s (%s)", sanitize.Log(fileName), err)
+ log.Errorf("config: failed loading values from %s (%s)", clean.Log(fileName), err)
c.AbortWithStatusJSON(http.StatusInternalServerError, err)
return
}
if err := yaml.Unmarshal(yamlData, v); err != nil {
- log.Warnf("config: failed parsing values in %s (%s)", sanitize.Log(fileName), err)
+ log.Warnf("config: failed parsing values in %s (%s)", clean.Log(fileName), err)
c.AbortWithStatusJSON(http.StatusInternalServerError, err)
return
}
@@ -122,14 +122,14 @@ func SaveConfigOptions(router *gin.RouterGroup) {
// Write YAML data to file.
if err := os.WriteFile(fileName, yamlData, os.ModePerm); err != nil {
- log.Errorf("config: failed writing values to %s (%s)", sanitize.Log(fileName), err)
+ log.Errorf("config: failed writing values to %s (%s)", clean.Log(fileName), err)
c.AbortWithStatusJSON(http.StatusInternalServerError, err)
return
}
// Reload options.
if err := conf.Options().Load(fileName); err != nil {
- log.Warnf("config: failed loading values from %s (%s)", sanitize.Log(fileName), err)
+ log.Warnf("config: failed loading values from %s (%s)", clean.Log(fileName), err)
c.AbortWithStatusJSON(http.StatusInternalServerError, err)
return
}
diff --git a/internal/api/covers.go b/internal/api/covers.go
index dd0080a69..d7230a04a 100644
--- a/internal/api/covers.go
+++ b/internal/api/covers.go
@@ -5,7 +5,7 @@ import (
"path/filepath"
"time"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
@@ -38,13 +38,13 @@ func AlbumCover(router *gin.RouterGroup) {
start := time.Now()
conf := service.Config()
- thumbName := thumb.Name(sanitize.Token(c.Param("size")))
- uid := sanitize.IdString(c.Param("uid"))
+ thumbName := thumb.Name(clean.Token(c.Param("size")))
+ uid := clean.IdString(c.Param("uid"))
size, ok := thumb.Sizes[thumbName]
if !ok {
- log.Errorf("%s: invalid size %s", albumCover, sanitize.Log(thumbName.String()))
+ log.Errorf("%s: invalid size %s", albumCover, clean.Log(thumbName.String()))
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
return
}
@@ -85,11 +85,11 @@ func AlbumCover(router *gin.RouterGroup) {
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
- log.Errorf("%s: found no original for %s", albumCover, sanitize.Log(fileName))
+ log.Errorf("%s: found no original for %s", albumCover, clean.Log(fileName))
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
- log.Warnf("%s: %s is missing", albumCover, sanitize.Log(f.FileName))
+ log.Warnf("%s: %s is missing", albumCover, clean.Log(f.FileName))
logError(albumCover, f.Update("FileMissing", true))
return
}
@@ -150,13 +150,13 @@ func LabelCover(router *gin.RouterGroup) {
start := time.Now()
conf := service.Config()
- thumbName := thumb.Name(sanitize.Token(c.Param("size")))
- uid := sanitize.IdString(c.Param("uid"))
+ thumbName := thumb.Name(clean.Token(c.Param("size")))
+ uid := clean.IdString(c.Param("uid"))
size, ok := thumb.Sizes[thumbName]
if !ok {
- log.Errorf("%s: invalid size %s", labelCover, sanitize.Log(thumbName.String()))
+ log.Errorf("%s: invalid size %s", labelCover, clean.Log(thumbName.String()))
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
return
}
@@ -197,7 +197,7 @@ func LabelCover(router *gin.RouterGroup) {
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
- log.Errorf("%s: file %s is missing", labelCover, sanitize.Log(f.FileName))
+ log.Errorf("%s: file %s is missing", labelCover, clean.Log(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
diff --git a/internal/api/download_album.go b/internal/api/download_album.go
index addcaf7fc..0624af84b 100644
--- a/internal/api/download_album.go
+++ b/internal/api/download_album.go
@@ -14,8 +14,8 @@ import (
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// DownloadAlbum streams the album contents as zip archive.
@@ -36,7 +36,7 @@ func DownloadAlbum(router *gin.RouterGroup) {
}
start := time.Now()
- a, err := query.AlbumByUID(sanitize.IdString(c.Param("uid")))
+ a, err := query.AlbumByUID(clean.IdString(c.Param("uid")))
if err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
@@ -57,26 +57,19 @@ func DownloadAlbum(router *gin.RouterGroup) {
zipWriter := zip.NewWriter(c.Writer)
defer zipWriter.Close()
- skipRaw := !conf.Settings().Download.Raw
-
var aliases = make(map[string]int)
for _, file := range files {
if file.FileHash == "" {
- log.Warnf("download: empty file hash, skipped %s", sanitize.Log(file.FileName))
+ log.Warnf("download: empty file hash, skipped %s", clean.Log(file.FileName))
continue
} else if file.FileName == "" {
- log.Warnf("download: empty file name, skipped %s", sanitize.Log(file.FileUID))
+ log.Warnf("download: empty file name, skipped %s", clean.Log(file.FileUID))
continue
}
if file.FileSidecar {
- log.Debugf("download: skipped sidecar %s", sanitize.Log(file.FileName))
- continue
- }
-
- if skipRaw && fs.FormatRaw.Is(file.FileType) {
- log.Debugf("download: skipped raw %s", sanitize.Log(file.FileName))
+ log.Debugf("download: skipped sidecar %s", clean.Log(file.FileName))
continue
}
@@ -92,17 +85,17 @@ func DownloadAlbum(router *gin.RouterGroup) {
if fs.FileExists(fileName) {
if err := addFileToZip(zipWriter, fileName, alias); err != nil {
- log.Errorf("download: failed adding %s to album zip (%s)", sanitize.Log(file.FileName), err)
+ log.Errorf("download: failed adding %s to album zip (%s)", clean.Log(file.FileName), err)
Abort(c, http.StatusInternalServerError, i18n.ErrZipFailed)
return
}
- log.Infof("download: added %s as %s", sanitize.Log(file.FileName), sanitize.Log(alias))
+ log.Infof("download: added %s as %s", clean.Log(file.FileName), clean.Log(alias))
} else {
- log.Warnf("download: album file %s is missing", sanitize.Log(file.FileName))
+ log.Warnf("download: album file %s is missing", clean.Log(file.FileName))
}
}
- log.Infof("download: created %s [%s]", sanitize.Log(zipFileName), time.Since(start))
+ log.Infof("download: created %s [%s]", clean.Log(zipFileName), time.Since(start))
})
}
diff --git a/internal/api/download_file.go b/internal/api/download_file.go
index 4488c1d96..2c9ec7015 100644
--- a/internal/api/download_file.go
+++ b/internal/api/download_file.go
@@ -10,8 +10,8 @@ import (
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// TODO: GET /api/v1/dl/file/:hash
@@ -45,7 +45,7 @@ func GetDownload(router *gin.RouterGroup) {
return
}
- fileHash := sanitize.Token(c.Param("hash"))
+ fileHash := clean.Token(c.Param("hash"))
f, err := query.FileByHash(fileHash)
@@ -57,7 +57,7 @@ func GetDownload(router *gin.RouterGroup) {
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
- log.Errorf("download: file %s is missing", sanitize.Log(f.FileName))
+ log.Errorf("download: file %s is missing", clean.Log(f.FileName))
c.Data(404, "image/svg+xml", brokenIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
diff --git a/internal/api/download_zip.go b/internal/api/download_zip.go
index 1350a11db..29faf4109 100644
--- a/internal/api/download_zip.go
+++ b/internal/api/download_zip.go
@@ -20,9 +20,9 @@ import (
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// CreateZip creates a zip file archive for download.
@@ -57,8 +57,17 @@ func CreateZip(router *gin.RouterGroup) {
return
}
- // Select files to be downloaded.
- files, err := query.SelectedFiles(f, query.FileSelectionAll())
+ // Configure file selection based on user settings.
+ var selection query.FileSelection
+ if dl := conf.Settings().Download; dl.Disabled {
+ AbortFeatureDisabled(c)
+ return
+ } else {
+ selection = query.DownloadSelection(dl.MediaRaw, dl.MediaSidecar, dl.Originals)
+ }
+
+ // Find files to download.
+ files, err := query.SelectedFiles(f, selection)
if err != nil {
Error(c, http.StatusBadRequest, err, i18n.ErrZipFailed)
@@ -68,53 +77,36 @@ func CreateZip(router *gin.RouterGroup) {
return
}
+ // Configure file names.
+ dlName := DownloadName(c)
zipPath := path.Join(conf.TempPath(), "zip")
- zipToken := rnd.Token(8)
+ zipToken := rnd.GenerateToken(8)
zipBaseName := fmt.Sprintf("photoprism-download-%s-%s.zip", time.Now().Format("20060102-150405"), zipToken)
zipFileName := path.Join(zipPath, zipBaseName)
+ // Create temp directory.
if err := os.MkdirAll(zipPath, 0700); err != nil {
Error(c, http.StatusInternalServerError, err, i18n.ErrZipFailed)
return
}
- newZipFile, err := os.Create(zipFileName)
-
- if err != nil {
+ // Create new zip file.
+ var newZipFile *os.File
+ if newZipFile, err = os.Create(zipFileName); err != nil {
Error(c, http.StatusInternalServerError, err, i18n.ErrZipFailed)
return
+ } else {
+ defer newZipFile.Close()
}
- defer newZipFile.Close()
-
+ // Create zip writer.
zipWriter := zip.NewWriter(newZipFile)
defer zipWriter.Close()
- dlName := DownloadName(c)
-
- skipRaw := !conf.Settings().Download.Raw
-
var aliases = make(map[string]int)
+ // Add files to zip.
for _, file := range files {
- if file.FileHash == "" {
- log.Warnf("download: empty file hash, skipped %s", sanitize.Log(file.FileName))
- continue
- } else if file.FileName == "" {
- log.Warnf("download: empty file name, skipped %s", sanitize.Log(file.FileUID))
- continue
- }
-
- if file.FileSidecar {
- log.Debugf("download: skipped sidecar %s", sanitize.Log(file.FileName))
- continue
- }
-
- if skipRaw && fs.FormatRaw.Is(file.FileType) {
- log.Debugf("download: skipped raw %s", sanitize.Log(file.FileName))
- continue
- }
-
fileName := photoprism.FileName(file.FileRoot, file.FileName)
alias := file.DownloadName(dlName, 0)
key := strings.ToLower(alias)
@@ -127,21 +119,21 @@ func CreateZip(router *gin.RouterGroup) {
if fs.FileExists(fileName) {
if err := addFileToZip(zipWriter, fileName, alias); err != nil {
- log.Errorf("download: failed adding %s to zip (%s)", sanitize.Log(file.FileName), err)
+ log.Errorf("download: failed adding %s to zip (%s)", clean.Log(file.FileName), err)
Abort(c, http.StatusInternalServerError, i18n.ErrZipFailed)
return
}
- log.Infof("download: added %s as %s", sanitize.Log(file.FileName), sanitize.Log(alias))
+ log.Infof("download: added %s as %s", clean.Log(file.FileName), clean.Log(alias))
} else {
- log.Warnf("download: media file %s is missing", sanitize.Log(file.FileName))
+ log.Warnf("download: media file %s is missing", clean.Log(file.FileName))
logError("download", file.Update("FileMissing", true))
}
}
elapsed := int(time.Since(start).Seconds())
- log.Infof("download: created %s [%s]", sanitize.Log(zipBaseName), time.Since(start))
+ log.Infof("download: created %s [%s]", clean.Log(zipBaseName), time.Since(start))
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgZipCreatedIn, elapsed), "filename": zipBaseName})
})
@@ -158,12 +150,12 @@ func DownloadZip(router *gin.RouterGroup) {
}
conf := service.Config()
- zipBaseName := sanitize.FileName(filepath.Base(c.Param("filename")))
+ zipBaseName := clean.FileName(filepath.Base(c.Param("filename")))
zipPath := path.Join(conf.TempPath(), "zip")
zipFileName := path.Join(zipPath, zipBaseName)
if !fs.FileExists(zipFileName) {
- log.Errorf("could not find zip file: %s", sanitize.Log(zipFileName))
+ log.Errorf("could not find zip file: %s", clean.Log(zipFileName))
c.Data(404, "image/svg+xml", photoIconSvg)
return
}
@@ -171,7 +163,7 @@ func DownloadZip(router *gin.RouterGroup) {
c.FileAttachment(zipFileName, zipBaseName)
if err := os.Remove(zipFileName); err != nil {
- log.Errorf("download: failed removing %s (%s)", sanitize.Log(zipFileName), err.Error())
+ log.Errorf("download: failed removing %s (%s)", clean.Log(zipFileName), err.Error())
}
})
}
diff --git a/internal/api/errors.go b/internal/api/errors.go
index 5959c2d7d..0149e99cf 100644
--- a/internal/api/errors.go
+++ b/internal/api/errors.go
@@ -31,7 +31,7 @@ func GetErrors(router *gin.RouterGroup) {
// Find and return matching logs.
if resp, err := query.Errors(limit, offset, c.Query("q")); err != nil {
- c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
return
} else {
AddCountHeader(c, len(resp))
diff --git a/internal/api/face.go b/internal/api/face.go
index 60d028979..3e118be99 100644
--- a/internal/api/face.go
+++ b/internal/api/face.go
@@ -3,7 +3,7 @@ package api
import (
"net/http"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
@@ -58,7 +58,7 @@ func UpdateFace(router *gin.RouterGroup) {
return
}
- faceId := sanitize.Token(c.Param("id"))
+ faceId := clean.Token(c.Param("id"))
m := entity.FindFace(faceId)
if m == nil {
@@ -70,7 +70,7 @@ func UpdateFace(router *gin.RouterGroup) {
if !f.FaceHidden && f.FaceHidden == m.FaceHidden {
// Do nothing.
} else if err := m.Update("FaceHidden", f.FaceHidden); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -78,7 +78,7 @@ func UpdateFace(router *gin.RouterGroup) {
if f.SubjUID == "" {
// Do nothing.
} else if err := m.SetSubjectUID(f.SubjUID); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/file.go b/internal/api/file.go
index aba4da250..bc8b3181f 100644
--- a/internal/api/file.go
+++ b/internal/api/file.go
@@ -3,7 +3,7 @@ package api
import (
"net/http"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
@@ -24,7 +24,7 @@ func GetFile(router *gin.RouterGroup) {
return
}
- p, err := query.FileByHash(sanitize.Token(c.Param("hash")))
+ p, err := query.FileByHash(clean.Token(c.Param("hash")))
if err != nil {
AbortEntityNotFound(c)
diff --git a/internal/api/file_delete.go b/internal/api/file_delete.go
index bfb7839ab..9375f71df 100644
--- a/internal/api/file_delete.go
+++ b/internal/api/file_delete.go
@@ -4,7 +4,7 @@ import (
"net/http"
"path/filepath"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
@@ -36,8 +36,8 @@ func DeleteFile(router *gin.RouterGroup) {
return
}
- photoUID := sanitize.IdString(c.Param("uid"))
- fileUID := sanitize.IdString(c.Param("file_uid"))
+ photoUID := clean.IdString(c.Param("uid"))
+ fileUID := clean.IdString(c.Param("file_uid"))
file, err := query.FileByUID(fileUID)
@@ -59,17 +59,17 @@ func DeleteFile(router *gin.RouterGroup) {
mediaFile, err := photoprism.NewMediaFile(fileName)
if err != nil {
- log.Errorf("photo: %s (delete %s)", err, sanitize.Log(baseName))
+ log.Errorf("photo: %s (delete %s)", err, clean.Log(baseName))
AbortEntityNotFound(c)
return
}
if err := mediaFile.Remove(); err != nil {
- log.Errorf("photo: %s (delete %s from folder)", err, sanitize.Log(baseName))
+ log.Errorf("photo: %s (delete %s from folder)", err, clean.Log(baseName))
}
if err := file.Delete(true); err != nil {
- log.Errorf("photo: %s (delete %s from index)", err, sanitize.Log(baseName))
+ log.Errorf("photo: %s (delete %s from index)", err, clean.Log(baseName))
AbortDeleteFailed(c)
return
}
diff --git a/internal/api/folder_cover.go b/internal/api/folder_cover.go
index 3fd787858..266ee1171 100644
--- a/internal/api/folder_cover.go
+++ b/internal/api/folder_cover.go
@@ -5,7 +5,7 @@ import (
"path/filepath"
"time"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
@@ -37,7 +37,7 @@ func FolderCover(router *gin.RouterGroup) {
start := time.Now()
conf := service.Config()
uid := c.Param("uid")
- thumbName := thumb.Name(sanitize.Token(c.Param("size")))
+ thumbName := thumb.Name(clean.Token(c.Param("size")))
download := c.Query("download") != ""
size, ok := thumb.Sizes[thumbName]
@@ -98,7 +98,7 @@ func FolderCover(router *gin.RouterGroup) {
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
- log.Warnf("%s: %s is missing", folderCover, sanitize.Log(f.FileName))
+ log.Warnf("%s: %s is missing", folderCover, clean.Log(f.FileName))
logError(folderCover, f.Update("FileMissing", true))
return
}
diff --git a/internal/api/import.go b/internal/api/import.go
index 447e44ef2..cb1f6c8bf 100644
--- a/internal/api/import.go
+++ b/internal/api/import.go
@@ -18,8 +18,8 @@ import (
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// StartImport imports media files from a directory and converts/indexes them as needed.
@@ -53,7 +53,7 @@ func StartImport(router *gin.RouterGroup) {
subPath := ""
path := conf.ImportPath()
- if subPath = sanitize.Path(c.Param("path")); subPath != "" && subPath != "/" {
+ if subPath = clean.Path(c.Param("path")); subPath != "" && subPath != "/" {
subPath = strings.Replace(subPath, ".", "", -1)
path = filepath.Join(path, subPath)
} else if f.Path != "" {
@@ -70,15 +70,15 @@ func StartImport(router *gin.RouterGroup) {
var opt photoprism.ImportOptions
if f.Move {
- event.InfoMsg(i18n.MsgMovingFilesFrom, sanitize.Log(filepath.Base(path)))
+ event.InfoMsg(i18n.MsgMovingFilesFrom, clean.Log(filepath.Base(path)))
opt = photoprism.ImportOptionsMove(path)
} else {
- event.InfoMsg(i18n.MsgCopyingFilesFrom, sanitize.Log(filepath.Base(path)))
+ event.InfoMsg(i18n.MsgCopyingFilesFrom, clean.Log(filepath.Base(path)))
opt = photoprism.ImportOptionsCopy(path)
}
if len(f.Albums) > 0 {
- log.Debugf("import: adding files to album %s", sanitize.Log(strings.Join(f.Albums, " and ")))
+ log.Debugf("import: adding files to album %s", clean.Log(strings.Join(f.Albums, " and ")))
opt.Albums = f.Albums
}
@@ -86,9 +86,9 @@ func StartImport(router *gin.RouterGroup) {
if subPath != "" && path != conf.ImportPath() && fs.IsEmpty(path) {
if err := os.Remove(path); err != nil {
- log.Errorf("import: failed deleting empty folder %s: %s", sanitize.Log(path), err)
+ log.Errorf("import: failed deleting empty folder %s: %s", clean.Log(path), err)
} else {
- log.Infof("import: deleted empty folder %s", sanitize.Log(path))
+ log.Infof("import: deleted empty folder %s", clean.Log(path))
}
}
diff --git a/internal/api/index.go b/internal/api/index.go
index 96a4a65b1..c5a50815d 100644
--- a/internal/api/index.go
+++ b/internal/api/index.go
@@ -13,7 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -53,7 +53,7 @@ func StartIndexing(router *gin.RouterGroup) {
indOpt := photoprism.NewIndexOptions(filepath.Clean(f.Path), f.Rescan, convert, true, false)
if len(indOpt.Path) > 1 {
- event.InfoMsg(i18n.MsgIndexingFiles, sanitize.Log(indOpt.Path))
+ event.InfoMsg(i18n.MsgIndexingFiles, clean.Log(indOpt.Path))
} else {
event.InfoMsg(i18n.MsgIndexingOriginals)
}
@@ -70,7 +70,7 @@ func StartIndexing(router *gin.RouterGroup) {
}
if files, photos, err := prg.Start(prgOpt); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
} else if len(files) > 0 || len(photos) > 0 {
event.InfoMsg(i18n.MsgRemovedFilesAndPhotos, len(files), len(photos))
diff --git a/internal/api/label.go b/internal/api/label.go
index e3ffee49c..8ab258ee6 100644
--- a/internal/api/label.go
+++ b/internal/api/label.go
@@ -3,7 +3,7 @@ package api
import (
"net/http"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
@@ -35,7 +35,7 @@ func UpdateLabel(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
m, err := query.LabelByUID(id)
if err != nil {
@@ -69,16 +69,16 @@ func LikeLabel(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
label, err := query.LabelByUID(id)
if err != nil {
- c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
if err := label.Update("LabelFavorite", true); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -109,16 +109,16 @@ func DislikeLabel(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
label, err := query.LabelByUID(id)
if err != nil {
- c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
if err := label.Update("LabelFavorite", false); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/link.go b/internal/api/link.go
index 3a1a3a647..ea9a8d624 100644
--- a/internal/api/link.go
+++ b/internal/api/link.go
@@ -13,7 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -33,7 +33,7 @@ func UpdateLink(c *gin.Context) {
return
}
- link := entity.FindLink(sanitize.Token(c.Param("link")))
+ link := entity.FindLink(clean.Token(c.Param("link")))
link.SetSlug(f.ShareSlug)
link.MaxViews = f.MaxViews
@@ -45,13 +45,13 @@ func UpdateLink(c *gin.Context) {
if f.Password != "" {
if err := link.SetPassword(f.Password); err != nil {
- c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
}
if err := link.Save(); err != nil {
- c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -73,10 +73,10 @@ func DeleteLink(c *gin.Context) {
return
}
- link := entity.FindLink(sanitize.Token(c.Param("link")))
+ link := entity.FindLink(clean.Token(c.Param("link")))
if err := link.Delete(); err != nil {
- c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -105,7 +105,7 @@ func CreateLink(c *gin.Context) {
return
}
- link := entity.NewLink(sanitize.IdString(c.Param("uid")), f.CanComment, f.CanEdit)
+ link := entity.NewLink(clean.IdString(c.Param("uid")), f.CanComment, f.CanEdit)
link.SetSlug(f.ShareSlug)
link.MaxViews = f.MaxViews
@@ -113,13 +113,13 @@ func CreateLink(c *gin.Context) {
if f.Password != "" {
if err := link.SetPassword(f.Password); err != nil {
- c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
}
if err := link.Save(); err != nil {
- c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusConflict, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -135,7 +135,7 @@ func CreateLink(c *gin.Context) {
// POST /api/v1/albums/:uid/links
func CreateAlbumLink(router *gin.RouterGroup) {
router.POST("/albums/:uid/links", func(c *gin.Context) {
- if _, err := query.AlbumByUID(sanitize.IdString(c.Param("uid"))); err != nil {
+ if _, err := query.AlbumByUID(clean.IdString(c.Param("uid"))); err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
return
}
@@ -161,7 +161,7 @@ func DeleteAlbumLink(router *gin.RouterGroup) {
// GET /api/v1/albums/:uid/links
func GetAlbumLinks(router *gin.RouterGroup) {
router.GET("/albums/:uid/links", func(c *gin.Context) {
- m, err := query.AlbumByUID(sanitize.IdString(c.Param("uid")))
+ m, err := query.AlbumByUID(clean.IdString(c.Param("uid")))
if err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
@@ -175,7 +175,7 @@ func GetAlbumLinks(router *gin.RouterGroup) {
// POST /api/v1/photos/:uid/links
func CreatePhotoLink(router *gin.RouterGroup) {
router.POST("/photos/:uid/links", func(c *gin.Context) {
- if _, err := query.PhotoByUID(sanitize.IdString(c.Param("uid"))); err != nil {
+ if _, err := query.PhotoByUID(clean.IdString(c.Param("uid"))); err != nil {
AbortEntityNotFound(c)
return
}
@@ -201,7 +201,7 @@ func DeletePhotoLink(router *gin.RouterGroup) {
// GET /api/v1/photos/:uid/links
func GetPhotoLinks(router *gin.RouterGroup) {
router.GET("/photos/:uid/links", func(c *gin.Context) {
- m, err := query.PhotoByUID(sanitize.IdString(c.Param("uid")))
+ m, err := query.PhotoByUID(clean.IdString(c.Param("uid")))
if err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
@@ -215,7 +215,7 @@ func GetPhotoLinks(router *gin.RouterGroup) {
// POST /api/v1/labels/:uid/links
func CreateLabelLink(router *gin.RouterGroup) {
router.POST("/labels/:uid/links", func(c *gin.Context) {
- if _, err := query.LabelByUID(sanitize.IdString(c.Param("uid"))); err != nil {
+ if _, err := query.LabelByUID(clean.IdString(c.Param("uid"))); err != nil {
Abort(c, http.StatusNotFound, i18n.ErrLabelNotFound)
return
}
@@ -241,7 +241,7 @@ func DeleteLabelLink(router *gin.RouterGroup) {
// GET /api/v1/labels/:uid/links
func GetLabelLinks(router *gin.RouterGroup) {
router.GET("/labels/:uid/links", func(c *gin.Context) {
- m, err := query.LabelByUID(sanitize.IdString(c.Param("uid")))
+ m, err := query.LabelByUID(clean.IdString(c.Param("uid")))
if err != nil {
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
diff --git a/internal/api/moments_time.go b/internal/api/moments_time.go
index 86a9412cd..564ba1b3b 100644
--- a/internal/api/moments_time.go
+++ b/internal/api/moments_time.go
@@ -23,7 +23,7 @@ func GetMomentsTime(router *gin.RouterGroup) {
result, err := query.MomentsTime(1)
if err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/photo.go b/internal/api/photo.go
index cd34f2e11..8236134cb 100644
--- a/internal/api/photo.go
+++ b/internal/api/photo.go
@@ -15,8 +15,8 @@ import (
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// SavePhotoAsYaml saves photo data as YAML file.
@@ -33,7 +33,7 @@ func SavePhotoAsYaml(p entity.Photo) {
if err := p.SaveAsYaml(fileName); err != nil {
log.Errorf("photo: %s (update yaml)", err)
} else {
- log.Debugf("photo: updated yaml file %s", sanitize.Log(filepath.Base(fileName)))
+ log.Debugf("photo: updated yaml file %s", clean.Log(filepath.Base(fileName)))
}
}
@@ -51,7 +51,7 @@ func GetPhoto(router *gin.RouterGroup) {
return
}
- p, err := query.PhotoPreloadByUID(sanitize.IdString(c.Param("uid")))
+ p, err := query.PhotoPreloadByUID(clean.IdString(c.Param("uid")))
if err != nil {
AbortEntityNotFound(c)
@@ -74,7 +74,7 @@ func UpdatePhoto(router *gin.RouterGroup) {
return
}
- uid := sanitize.IdString(c.Param("uid"))
+ uid := clean.IdString(c.Param("uid"))
m, err := query.PhotoByUID(uid)
if err != nil {
@@ -136,7 +136,7 @@ func GetPhotoDownload(router *gin.RouterGroup) {
return
}
- f, err := query.FileByPhotoUID(sanitize.IdString(c.Param("uid")))
+ f, err := query.FileByPhotoUID(clean.IdString(c.Param("uid")))
if err != nil {
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
@@ -146,7 +146,7 @@ func GetPhotoDownload(router *gin.RouterGroup) {
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
- log.Errorf("photo: file %s is missing", sanitize.Log(f.FileName))
+ log.Errorf("photo: file %s is missing", clean.Log(f.FileName))
c.Data(http.StatusNotFound, "image/svg+xml", photoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
@@ -172,7 +172,7 @@ func GetPhotoYaml(router *gin.RouterGroup) {
return
}
- p, err := query.PhotoPreloadByUID(sanitize.IdString(c.Param("uid")))
+ p, err := query.PhotoPreloadByUID(clean.IdString(c.Param("uid")))
if err != nil {
c.AbortWithStatus(http.StatusNotFound)
@@ -187,7 +187,7 @@ func GetPhotoYaml(router *gin.RouterGroup) {
}
if c.Query("download") != "" {
- AddDownloadHeader(c, sanitize.IdString(c.Param("uid"))+fs.YamlExt)
+ AddDownloadHeader(c, clean.IdString(c.Param("uid"))+fs.ExtYAML)
}
c.Data(http.StatusOK, "text/x-yaml; charset=utf-8", data)
@@ -207,7 +207,7 @@ func ApprovePhoto(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
m, err := query.PhotoByUID(id)
if err != nil {
@@ -242,7 +242,7 @@ func LikePhoto(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
m, err := query.PhotoByUID(id)
if err != nil {
@@ -277,7 +277,7 @@ func DislikePhoto(router *gin.RouterGroup) {
return
}
- id := sanitize.IdString(c.Param("uid"))
+ id := clean.IdString(c.Param("uid"))
m, err := query.PhotoByUID(id)
if err != nil {
@@ -313,8 +313,8 @@ func PhotoPrimary(router *gin.RouterGroup) {
return
}
- uid := sanitize.IdString(c.Param("uid"))
- fileUID := sanitize.IdString(c.Param("file_uid"))
+ uid := clean.IdString(c.Param("uid"))
+ fileUID := clean.IdString(c.Param("file_uid"))
err := query.SetPhotoPrimary(uid, fileUID)
if err != nil {
diff --git a/internal/api/photo_label.go b/internal/api/photo_label.go
index 6b069b3a4..781321da2 100644
--- a/internal/api/photo_label.go
+++ b/internal/api/photo_label.go
@@ -4,7 +4,7 @@ import (
"net/http"
"strconv"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
@@ -29,7 +29,7 @@ func AddPhotoLabel(router *gin.RouterGroup) {
return
}
- m, err := query.PhotoByUID(sanitize.IdString(c.Param("uid")))
+ m, err := query.PhotoByUID(clean.IdString(c.Param("uid")))
if err != nil {
AbortEntityNotFound(c)
@@ -70,7 +70,7 @@ func AddPhotoLabel(router *gin.RouterGroup) {
}
}
- p, err := query.PhotoPreloadByUID(sanitize.IdString(c.Param("uid")))
+ p, err := query.PhotoPreloadByUID(clean.IdString(c.Param("uid")))
if err != nil {
AbortEntityNotFound(c)
@@ -78,7 +78,7 @@ func AddPhotoLabel(router *gin.RouterGroup) {
}
if err := p.SaveLabels(); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -104,24 +104,24 @@ func RemovePhotoLabel(router *gin.RouterGroup) {
return
}
- m, err := query.PhotoByUID(sanitize.IdString(c.Param("uid")))
+ m, err := query.PhotoByUID(clean.IdString(c.Param("uid")))
if err != nil {
AbortEntityNotFound(c)
return
}
- labelId, err := strconv.Atoi(sanitize.Token(c.Param("id")))
+ labelId, err := strconv.Atoi(clean.Token(c.Param("id")))
if err != nil {
- c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
label, err := query.PhotoLabel(m.ID, uint(labelId))
if err != nil {
- c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -132,7 +132,7 @@ func RemovePhotoLabel(router *gin.RouterGroup) {
logError("label", entity.Db().Save(&label).Error)
}
- p, err := query.PhotoPreloadByUID(sanitize.IdString(c.Param("uid")))
+ p, err := query.PhotoPreloadByUID(clean.IdString(c.Param("uid")))
if err != nil {
AbortEntityNotFound(c)
@@ -142,11 +142,11 @@ func RemovePhotoLabel(router *gin.RouterGroup) {
logError("label", p.RemoveKeyword(label.Label.LabelName))
if err := p.SaveLabels(); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
- PublishPhotoEvent(EntityUpdated, sanitize.IdString(c.Param("uid")), c)
+ PublishPhotoEvent(EntityUpdated, clean.IdString(c.Param("uid")), c)
event.Success("label removed")
@@ -170,24 +170,24 @@ func UpdatePhotoLabel(router *gin.RouterGroup) {
// TODO: Code clean-up, simplify
- m, err := query.PhotoByUID(sanitize.IdString(c.Param("uid")))
+ m, err := query.PhotoByUID(clean.IdString(c.Param("uid")))
if err != nil {
AbortEntityNotFound(c)
return
}
- labelId, err := strconv.Atoi(sanitize.Token(c.Param("id")))
+ labelId, err := strconv.Atoi(clean.Token(c.Param("id")))
if err != nil {
- c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
label, err := query.PhotoLabel(m.ID, uint(labelId))
if err != nil {
- c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -197,11 +197,11 @@ func UpdatePhotoLabel(router *gin.RouterGroup) {
}
if err := label.Save(); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
- p, err := query.PhotoPreloadByUID(sanitize.IdString(c.Param("uid")))
+ p, err := query.PhotoPreloadByUID(clean.IdString(c.Param("uid")))
if err != nil {
AbortEntityNotFound(c)
@@ -209,11 +209,11 @@ func UpdatePhotoLabel(router *gin.RouterGroup) {
}
if err := p.SaveLabels(); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
- PublishPhotoEvent(EntityUpdated, sanitize.IdString(c.Param("uid")), c)
+ PublishPhotoEvent(EntityUpdated, clean.IdString(c.Param("uid")), c)
event.Success("label saved")
diff --git a/internal/api/photo_unstack.go b/internal/api/photo_unstack.go
index cf705dbb2..8ebf21c89 100644
--- a/internal/api/photo_unstack.go
+++ b/internal/api/photo_unstack.go
@@ -5,7 +5,7 @@ import (
"net/http"
"path/filepath"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
@@ -34,7 +34,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
}
conf := service.Config()
- fileUID := sanitize.IdString(c.Param("file_uid"))
+ fileUID := clean.IdString(c.Param("file_uid"))
file, err := query.FileByUID(fileUID)
if err != nil {
@@ -63,7 +63,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
unstackFile, err := photoprism.NewMediaFile(fileName)
if err != nil {
- log.Errorf("photo: %s (unstack %s)", err, sanitize.Log(baseName))
+ log.Errorf("photo: %s (unstack %s)", err, clean.Log(baseName))
AbortEntityNotFound(c)
return
} else if file.Photo == nil {
@@ -76,7 +76,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
stackPrimary, err := stackPhoto.PrimaryFile()
if err != nil {
- log.Errorf("photo: cannot find primary file for %s (unstack)", sanitize.Log(baseName))
+ log.Errorf("photo: cannot find primary file for %s (unstack)", clean.Log(baseName))
AbortUnexpected(c)
return
}
@@ -87,15 +87,15 @@ func PhotoUnstack(router *gin.RouterGroup) {
related, err := unstackFile.RelatedFiles(false)
if err != nil {
- log.Errorf("photo: %s (unstack %s)", err, sanitize.Log(baseName))
+ log.Errorf("photo: %s (unstack %s)", err, clean.Log(baseName))
AbortEntityNotFound(c)
return
} else if related.Len() == 0 {
- log.Errorf("photo: found no files for %s (unstack)", sanitize.Log(baseName))
+ log.Errorf("photo: found no files for %s (unstack)", clean.Log(baseName))
AbortEntityNotFound(c)
return
} else if related.Main == nil {
- log.Errorf("photo: found no main file for %s (unstack)", sanitize.Log(baseName))
+ log.Errorf("photo: found no main file for %s (unstack)", clean.Log(baseName))
AbortEntityNotFound(c)
return
}
@@ -105,7 +105,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
if unstackFile.BasePrefix(false) == stackPhoto.PhotoName {
if conf.ReadOnly() {
- log.Errorf("photo: cannot rename files in read only mode (unstack %s)", sanitize.Log(baseName))
+ log.Errorf("photo: cannot rename files in read only mode (unstack %s)", clean.Log(baseName))
AbortFeatureDisabled(c)
return
}
@@ -113,7 +113,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
destName := fmt.Sprintf("%s.%s%s", unstackFile.AbsPrefix(false), unstackFile.Checksum(), unstackFile.Extension())
if err := unstackFile.Move(destName); err != nil {
- log.Errorf("photo: cannot rename %s to %s (unstack)", sanitize.Log(unstackFile.BaseName()), sanitize.Log(filepath.Base(destName)))
+ log.Errorf("photo: cannot rename %s to %s (unstack)", clean.Log(unstackFile.BaseName()), clean.Log(filepath.Base(destName)))
AbortUnexpected(c)
return
}
@@ -130,7 +130,7 @@ func PhotoUnstack(router *gin.RouterGroup) {
newPhoto.PhotoName = unstackFile.BasePrefix(false)
if err := newPhoto.Create(); err != nil {
- log.Errorf("photo: %s (unstack %s)", err.Error(), sanitize.Log(baseName))
+ log.Errorf("photo: %s (unstack %s)", err.Error(), clean.Log(baseName))
AbortSaveFailed(c)
return
}
@@ -150,17 +150,17 @@ func PhotoUnstack(router *gin.RouterGroup) {
newPhoto.ID, newPhoto.PhotoUID, r.RootRelName(),
relName, relRoot).Error; err != nil {
// Handle error...
- log.Errorf("photo: %s (unstack %s)", err.Error(), sanitize.Log(r.BaseName()))
+ log.Errorf("photo: %s (unstack %s)", err.Error(), clean.Log(r.BaseName()))
// Remove new photo from index.
if _, err := newPhoto.Delete(true); err != nil {
- log.Errorf("photo: %s (unstack %s)", err.Error(), sanitize.Log(r.BaseName()))
+ log.Errorf("photo: %s (unstack %s)", err.Error(), clean.Log(r.BaseName()))
}
// Revert file rename.
if unstackSingle {
if err := r.Move(photoprism.FileName(relRoot, relName)); err != nil {
- log.Errorf("photo: %s (unstack %s)", err.Error(), sanitize.Log(r.BaseName()))
+ log.Errorf("photo: %s (unstack %s)", err.Error(), clean.Log(r.BaseName()))
}
}
@@ -173,21 +173,21 @@ func PhotoUnstack(router *gin.RouterGroup) {
// Index unstacked files.
if res := ind.FileName(unstackFile.FileName(), photoprism.IndexOptionsSingle()); res.Failed() {
- log.Errorf("photo: %s (unstack %s)", res.Err, sanitize.Log(baseName))
+ log.Errorf("photo: %s (unstack %s)", res.Err, clean.Log(baseName))
AbortSaveFailed(c)
return
}
// Reset type for existing photo stack to image.
if err := stackPhoto.Update("PhotoType", entity.MediaImage); err != nil {
- log.Errorf("photo: %s (unstack %s)", err, sanitize.Log(baseName))
+ log.Errorf("photo: %s (unstack %s)", err, clean.Log(baseName))
AbortUnexpected(c)
return
}
// Re-index existing photo stack.
if res := ind.FileName(photoprism.FileName(stackPrimary.FileRoot, stackPrimary.FileName), photoprism.IndexOptionsSingle()); res.Failed() {
- log.Errorf("photo: %s (unstack %s)", res.Err, sanitize.Log(baseName))
+ log.Errorf("photo: %s (unstack %s)", res.Err, clean.Log(baseName))
AbortSaveFailed(c)
return
}
diff --git a/internal/api/search_albums.go b/internal/api/search_albums.go
index 427cb7501..c216e5d60 100644
--- a/internal/api/search_albums.go
+++ b/internal/api/search_albums.go
@@ -41,7 +41,7 @@ func SearchAlbums(router *gin.RouterGroup) {
result, err := search.Albums(f)
if err != nil {
- c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/search_faces.go b/internal/api/search_faces.go
index b09b30ebb..140ffe590 100644
--- a/internal/api/search_faces.go
+++ b/internal/api/search_faces.go
@@ -36,7 +36,7 @@ func SearchFaces(router *gin.RouterGroup) {
result, err := search.Faces(f)
if err != nil {
- c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/search_folders.go b/internal/api/search_folders.go
index 2887d9f1a..4499efbd7 100644
--- a/internal/api/search_folders.go
+++ b/internal/api/search_folders.go
@@ -6,7 +6,7 @@ import (
"path/filepath"
"time"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
@@ -68,7 +68,7 @@ func SearchFolders(router *gin.RouterGroup, urlPath, rootName, rootPath string)
listFiles := f.Files
uncached := listFiles || f.Uncached
resp := FoldersResponse{Root: rootName, Recursive: recursive, Cached: !uncached}
- path := sanitize.Path(c.Param("path"))
+ path := clean.Path(c.Param("path"))
cacheKey := fmt.Sprintf("folder:%s:%t:%t", filepath.Join(rootName, path), recursive, listFiles)
diff --git a/internal/api/search_geojson.go b/internal/api/search_geojson.go
index 7b4ff4eaf..91e18b419 100644
--- a/internal/api/search_geojson.go
+++ b/internal/api/search_geojson.go
@@ -11,7 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/search"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -66,7 +66,7 @@ func SearchGeo(router *gin.RouterGroup) {
var resp []byte
// Render JSON response.
- switch sanitize.Token(c.Param("format")) {
+ switch clean.Token(c.Param("format")) {
case "view":
conf := service.Config()
resp, err = photos.ViewerJSON(conf.ContentUri(), conf.ApiUri(), conf.PreviewToken(), conf.DownloadToken())
@@ -75,7 +75,7 @@ func SearchGeo(router *gin.RouterGroup) {
}
if err != nil {
- c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/search_labels.go b/internal/api/search_labels.go
index 59f4b667f..5f6ce8c19 100644
--- a/internal/api/search_labels.go
+++ b/internal/api/search_labels.go
@@ -35,7 +35,7 @@ func SearchLabels(router *gin.RouterGroup) {
result, err := search.Labels(f)
if err != nil {
- c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/search_subjects.go b/internal/api/search_subjects.go
index e507f5a4d..a48538172 100644
--- a/internal/api/search_subjects.go
+++ b/internal/api/search_subjects.go
@@ -36,7 +36,7 @@ func SearchSubjects(router *gin.RouterGroup) {
result, err := search.Subjects(f)
if err != nil {
- c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(400, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/session.go b/internal/api/session.go
index 8877164e3..842fb1829 100644
--- a/internal/api/session.go
+++ b/internal/api/session.go
@@ -3,7 +3,7 @@ package api
import (
"net/http"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
@@ -91,7 +91,7 @@ func CreateSession(router *gin.RouterGroup) {
// DELETE /api/v1/session/:id
func DeleteSession(router *gin.RouterGroup) {
router.DELETE("/session/:id", func(c *gin.Context) {
- id := sanitize.Token(c.Param("id"))
+ id := clean.Token(c.Param("id"))
service.Session().Delete(id)
@@ -128,10 +128,10 @@ func Auth(id string, resource acl.Resource, action acl.Action) session.Data {
// InvalidPreviewToken returns true if the token is invalid.
func InvalidPreviewToken(c *gin.Context) bool {
- token := sanitize.Token(c.Param("token"))
+ token := clean.Token(c.Param("token"))
if token == "" {
- token = sanitize.Token(c.Query("t"))
+ token = clean.Token(c.Query("t"))
}
return service.Config().InvalidPreviewToken(token)
@@ -139,5 +139,5 @@ func InvalidPreviewToken(c *gin.Context) bool {
// InvalidDownloadToken returns true if the token is invalid.
func InvalidDownloadToken(c *gin.Context) bool {
- return service.Config().InvalidDownloadToken(sanitize.Token(c.Query("t")))
+ return service.Config().InvalidDownloadToken(clean.Token(c.Query("t")))
}
diff --git a/internal/api/share.go b/internal/api/share.go
index df6aefb40..8a7d54522 100644
--- a/internal/api/share.go
+++ b/internal/api/share.go
@@ -9,7 +9,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// GET /s/:token/...
@@ -17,7 +17,7 @@ func Shares(router *gin.RouterGroup) {
router.GET("/:token", func(c *gin.Context) {
conf := service.Config()
- token := sanitize.Token(c.Param("token"))
+ token := clean.Token(c.Param("token"))
links := entity.FindValidLinks(token, "")
@@ -36,8 +36,8 @@ func Shares(router *gin.RouterGroup) {
router.GET("/:token/:share", func(c *gin.Context) {
conf := service.Config()
- token := sanitize.Token(c.Param("token"))
- share := sanitize.Token(c.Param("share"))
+ token := clean.Token(c.Param("token"))
+ share := clean.Token(c.Param("share"))
links := entity.FindValidLinks(token, share)
diff --git a/internal/api/share_preview.go b/internal/api/share_preview.go
index f330d4089..40bb968e4 100644
--- a/internal/api/share_preview.go
+++ b/internal/api/share_preview.go
@@ -9,7 +9,7 @@ import (
"path"
"time"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
@@ -30,8 +30,8 @@ func SharePreview(router *gin.RouterGroup) {
router.GET("/:token/:share/preview", func(c *gin.Context) {
conf := service.Config()
- token := sanitize.Token(c.Param("token"))
- share := sanitize.Token(c.Param("share"))
+ token := clean.Token(c.Param("token"))
+ share := clean.Token(c.Param("share"))
links := entity.FindLinks(token, share)
if len(links) != 1 {
@@ -52,13 +52,13 @@ func SharePreview(router *gin.RouterGroup) {
yesterday := time.Now().Add(-24 * time.Hour)
if info, err := os.Stat(previewFilename); err != nil {
- log.Debugf("share: creating new preview for %s", sanitize.Log(share))
+ log.Debugf("share: creating new preview for %s", clean.Log(share))
} else if info.ModTime().After(yesterday) {
- log.Debugf("share: using cached preview for %s", sanitize.Log(share))
+ log.Debugf("share: using cached preview for %s", clean.Log(share))
c.File(previewFilename)
return
} else if err := os.Remove(previewFilename); err != nil {
- log.Errorf("share: could not remove old preview of %s", sanitize.Log(share))
+ log.Errorf("share: could not remove old preview of %s", clean.Log(share))
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
return
}
@@ -96,7 +96,7 @@ func SharePreview(router *gin.RouterGroup) {
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
- log.Errorf("share: file %s is missing (preview)", sanitize.Log(f.FileName))
+ log.Errorf("share: file %s is missing (preview)", clean.Log(f.FileName))
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
return
}
@@ -126,7 +126,7 @@ func SharePreview(router *gin.RouterGroup) {
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
- log.Errorf("share: file %s is missing (preview)", sanitize.Log(f.FileName))
+ log.Errorf("share: file %s is missing (preview)", clean.Log(f.FileName))
c.Redirect(http.StatusTemporaryRedirect, conf.SitePreview())
return
}
diff --git a/internal/api/subject.go b/internal/api/subject.go
index 7e8077d47..d149a528b 100644
--- a/internal/api/subject.go
+++ b/internal/api/subject.go
@@ -3,7 +3,7 @@ package api
import (
"net/http"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
@@ -28,7 +28,7 @@ func GetSubject(router *gin.RouterGroup) {
return
}
- if subj := entity.FindSubject(sanitize.IdString(c.Param("uid"))); subj == nil {
+ if subj := entity.FindSubject(clean.IdString(c.Param("uid"))); subj == nil {
Abort(c, http.StatusNotFound, i18n.ErrSubjectNotFound)
return
} else {
@@ -56,7 +56,7 @@ func UpdateSubject(router *gin.RouterGroup) {
return
}
- uid := sanitize.IdString(c.Param("uid"))
+ uid := clean.IdString(c.Param("uid"))
m := entity.FindSubject(uid)
if m == nil {
@@ -109,7 +109,7 @@ func LikeSubject(router *gin.RouterGroup) {
return
}
- uid := sanitize.IdString(c.Param("uid"))
+ uid := clean.IdString(c.Param("uid"))
subj := entity.FindSubject(uid)
if subj == nil {
@@ -118,7 +118,7 @@ func LikeSubject(router *gin.RouterGroup) {
}
if err := subj.Update("SubjFavorite", true); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
@@ -143,7 +143,7 @@ func DislikeSubject(router *gin.RouterGroup) {
return
}
- uid := sanitize.IdString(c.Param("uid"))
+ uid := clean.IdString(c.Param("uid"))
subj := entity.FindSubject(uid)
if subj == nil {
@@ -152,7 +152,7 @@ func DislikeSubject(router *gin.RouterGroup) {
}
if err := subj.Update("SubjFavorite", false); err != nil {
- c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
+ c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UpperFirst(err.Error())})
return
}
diff --git a/internal/api/thumbnails.go b/internal/api/thumbnails.go
index a2ad6ddeb..e6b234608 100644
--- a/internal/api/thumbnails.go
+++ b/internal/api/thumbnails.go
@@ -13,8 +13,8 @@ import (
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/thumb"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// GetThumb returns a thumbnail image matching the file hash, crop area, and type.
@@ -37,16 +37,16 @@ func GetThumb(router *gin.RouterGroup) {
start := time.Now()
conf := service.Config()
download := c.Query("download") != ""
- fileHash, cropArea := crop.ParseThumb(sanitize.Token(c.Param("thumb")))
+ fileHash, cropArea := crop.ParseThumb(clean.Token(c.Param("thumb")))
// Is cropped thumbnail?
if cropArea != "" {
- cropName := crop.Name(sanitize.Token(c.Param("size")))
+ cropName := crop.Name(clean.Token(c.Param("size")))
cropSize, ok := crop.Sizes[cropName]
if !ok {
- log.Errorf("%s: invalid size %s", logPrefix, sanitize.Log(string(cropName)))
+ log.Errorf("%s: invalid size %s", logPrefix, clean.Log(string(cropName)))
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
@@ -73,12 +73,12 @@ func GetThumb(router *gin.RouterGroup) {
return
}
- thumbName := thumb.Name(sanitize.Token(c.Param("size")))
+ thumbName := thumb.Name(clean.Token(c.Param("size")))
size, ok := thumb.Sizes[thumbName]
if !ok {
- log.Errorf("%s: invalid size %s", logPrefix, sanitize.Log(thumbName.String()))
+ log.Errorf("%s: invalid size %s", logPrefix, clean.Log(thumbName.String()))
c.Data(http.StatusOK, "image/svg+xml", photoIconSvg)
return
}
@@ -153,17 +153,17 @@ func GetThumb(router *gin.RouterGroup) {
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if !fs.FileExists(fileName) {
- log.Errorf("%s: file %s is missing", logPrefix, sanitize.Log(f.FileName))
+ log.Errorf("%s: file %s is missing", logPrefix, clean.Log(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
logError(logPrefix, f.Update("FileMissing", true))
if f.AllFilesMissing() {
- log.Infof("%s: deleting photo, all files missing for %s", logPrefix, sanitize.Log(f.FileName))
+ log.Infof("%s: deleting photo, all files missing for %s", logPrefix, clean.Log(f.FileName))
if _, err := f.RelatedPhoto().Delete(false); err != nil {
- log.Errorf("%s: %s while deleting %s", logPrefix, err, sanitize.Log(f.FileName))
+ log.Errorf("%s: %s while deleting %s", logPrefix, err, clean.Log(f.FileName))
}
}
diff --git a/internal/api/upload.go b/internal/api/upload.go
index 1a9644e32..70aa56687 100644
--- a/internal/api/upload.go
+++ b/internal/api/upload.go
@@ -12,7 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// POST /api/v1/upload/:path
@@ -32,7 +32,7 @@ func Upload(router *gin.RouterGroup) {
}
start := time.Now()
- subPath := sanitize.Path(c.Param("path"))
+ subPath := clean.Path(c.Param("path"))
f, err := c.MultipartForm()
@@ -51,7 +51,7 @@ func Upload(router *gin.RouterGroup) {
p := path.Join(conf.ImportPath(), "upload", subPath)
if err := os.MkdirAll(p, os.ModePerm); err != nil {
- log.Errorf("upload: failed creating folder %s", sanitize.Log(subPath))
+ log.Errorf("upload: failed creating folder %s", clean.Log(subPath))
AbortBadRequest(c)
return
}
@@ -59,10 +59,10 @@ func Upload(router *gin.RouterGroup) {
for _, file := range files {
filename := path.Join(p, filepath.Base(file.Filename))
- log.Debugf("upload: saving file %s", sanitize.Log(file.Filename))
+ log.Debugf("upload: saving file %s", clean.Log(file.Filename))
if err := c.SaveUploadedFile(file, filename); err != nil {
- log.Errorf("upload: failed saving file %s", sanitize.Log(filepath.Base(file.Filename)))
+ log.Errorf("upload: failed saving file %s", clean.Log(filepath.Base(file.Filename)))
AbortBadRequest(c)
return
}
@@ -87,7 +87,7 @@ func Upload(router *gin.RouterGroup) {
continue
}
- log.Infof("nsfw: %s might be offensive", sanitize.Log(filename))
+ log.Infof("nsfw: %s might be offensive", clean.Log(filename))
containsNSFW = true
}
@@ -95,7 +95,7 @@ func Upload(router *gin.RouterGroup) {
if containsNSFW {
for _, filename := range uploads {
if err := os.Remove(filename); err != nil {
- log.Errorf("nsfw: could not delete %s", sanitize.Log(filename))
+ log.Errorf("nsfw: could not delete %s", clean.Log(filename))
}
}
diff --git a/internal/api/user_password.go b/internal/api/user_password.go
index cf024cd02..ad07903a9 100644
--- a/internal/api/user_password.go
+++ b/internal/api/user_password.go
@@ -3,7 +3,7 @@ package api
import (
"net/http"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
@@ -30,7 +30,7 @@ func ChangePassword(router *gin.RouterGroup) {
return
}
- uid := sanitize.IdString(c.Param("uid"))
+ uid := clean.IdString(c.Param("uid"))
m := entity.FindUserByUID(uid)
if s.User.UserUID != m.UserUID {
diff --git a/internal/api/video.go b/internal/api/video.go
index 0c4dc6365..2bcb8f675 100644
--- a/internal/api/video.go
+++ b/internal/api/video.go
@@ -3,13 +3,14 @@ package api
import (
"net/http"
+ "github.com/photoprism/photoprism/pkg/video"
+
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/internal/video"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// GetVideo streams videos.
@@ -26,13 +27,13 @@ func GetVideo(router *gin.RouterGroup) {
return
}
- fileHash := sanitize.Token(c.Param("hash"))
- formatName := sanitize.Token(c.Param("format"))
+ fileHash := clean.Token(c.Param("hash"))
+ formatName := clean.Token(c.Param("format"))
- format, ok := video.Formats[formatName]
+ format, ok := video.Types[formatName]
if !ok {
- log.Errorf("video: invalid format %s", sanitize.Log(formatName))
+ log.Errorf("video: invalid format %s", clean.Log(formatName))
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
return
}
@@ -64,7 +65,7 @@ func GetVideo(router *gin.RouterGroup) {
fileName := photoprism.FileName(f.FileRoot, f.FileName)
if mf, err := photoprism.NewMediaFile(fileName); err != nil {
- log.Errorf("video: file %s is missing", sanitize.Log(f.FileName))
+ log.Errorf("video: file %s is missing", clean.Log(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
// Set missing flag so that the file doesn't show up in search results anymore.
@@ -75,7 +76,7 @@ func GetVideo(router *gin.RouterGroup) {
conv := service.Convert()
if avcFile, err := conv.ToAvc(mf, service.Config().FFmpegEncoder(), false, false); err != nil {
- log.Errorf("video: transcoding %s failed", sanitize.Log(f.FileName))
+ log.Errorf("video: transcoding %s failed", clean.Log(f.FileName))
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
return
} else {
diff --git a/internal/auto/import.go b/internal/auto/import.go
index 019be0eb5..9a964c75c 100644
--- a/internal/auto/import.go
+++ b/internal/auto/import.go
@@ -12,7 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
var autoImport = time.Time{}
@@ -66,7 +66,7 @@ func Import() error {
api.RemoveFromFolderCache(entity.RootImport)
- event.InfoMsg(i18n.MsgCopyingFilesFrom, sanitize.Log(filepath.Base(path)))
+ event.InfoMsg(i18n.MsgCopyingFilesFrom, clean.Log(filepath.Base(path)))
var opt photoprism.ImportOptions
diff --git a/internal/classify/gen.go b/internal/classify/gen.go
index 25e2f5697..df357249d 100644
--- a/internal/classify/gen.go
+++ b/internal/classify/gen.go
@@ -11,8 +11,8 @@ import (
"text/template"
"unicode"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
"gopkg.in/yaml.v2"
)
@@ -34,7 +34,7 @@ func main() {
fileName := "rules.yml"
if !fs.FileExists(fileName) {
- log.Panicf("classify: found no label rules in %s", sanitize.Log(filepath.Base(fileName)))
+ log.Panicf("classify: found no label rules in %s", clean.Log(filepath.Base(fileName)))
}
yamlConfig, err := os.ReadFile(fileName)
diff --git a/internal/classify/tensorflow.go b/internal/classify/tensorflow.go
index d1f48c52b..e831a8d64 100644
--- a/internal/classify/tensorflow.go
+++ b/internal/classify/tensorflow.go
@@ -14,7 +14,7 @@ import (
"strings"
"github.com/disintegration/imaging"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
)
@@ -148,7 +148,7 @@ func (t *TensorFlow) loadModel() error {
modelPath := path.Join(t.modelsPath, t.modelName)
- log.Infof("classify: loading %s", sanitize.Log(filepath.Base(modelPath)))
+ log.Infof("classify: loading %s", clean.Log(filepath.Base(modelPath)))
// Load model
model, err := tf.LoadSavedModel(modelPath, t.modelTags, nil)
diff --git a/internal/commands/backup.go b/internal/commands/backup.go
index dc76b2001..1a2443b17 100644
--- a/internal/commands/backup.go
+++ b/internal/commands/backup.go
@@ -16,8 +16,8 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
const backupDescription = "A user-defined SQL dump FILENAME or - for stdout can be passed as the first argument. " +
@@ -113,7 +113,7 @@ func backupAction(ctx *cli.Context) error {
}
}
- log.Infof("writing SQL dump to %s", sanitize.Log(indexFileName))
+ log.Infof("writing SQL dump to %s", clean.Log(indexFileName))
}
var cmd *exec.Cmd
@@ -178,7 +178,7 @@ func backupAction(ctx *cli.Context) error {
albumsPath = conf.AlbumsPath()
}
- log.Infof("saving albums in %s", sanitize.Log(albumsPath))
+ log.Infof("saving albums in %s", clean.Log(albumsPath))
if count, err := photoprism.BackupAlbums(albumsPath, true); err != nil {
return err
diff --git a/internal/commands/convert.go b/internal/commands/convert.go
index 155bcdbda..3a4ed326c 100644
--- a/internal/commands/convert.go
+++ b/internal/commands/convert.go
@@ -10,7 +10,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// ConvertCommand registers the convert cli command.
@@ -54,7 +54,7 @@ func convertAction(ctx *cli.Context) error {
convertPath = filepath.Join(convertPath, subPath)
}
- log.Infof("converting originals in %s", sanitize.Log(convertPath))
+ log.Infof("converting originals in %s", clean.Log(convertPath))
w := service.Convert()
diff --git a/internal/commands/faces.go b/internal/commands/faces.go
index e134bef26..12155486c 100644
--- a/internal/commands/faces.go
+++ b/internal/commands/faces.go
@@ -14,8 +14,8 @@ import (
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// FacesCommand registers the facial recognition subcommands.
@@ -239,9 +239,9 @@ func facesIndexAction(ctx *cli.Context) error {
subPath := strings.TrimSpace(ctx.Args().First())
if subPath == "" {
- log.Infof("finding faces in %s", sanitize.Log(conf.OriginalsPath()))
+ log.Infof("finding faces in %s", clean.Log(conf.OriginalsPath()))
} else {
- log.Infof("finding faces in %s", sanitize.Log(filepath.Join(conf.OriginalsPath(), subPath)))
+ log.Infof("finding faces in %s", clean.Log(filepath.Join(conf.OriginalsPath(), subPath)))
}
if conf.ReadOnly() {
diff --git a/internal/commands/index.go b/internal/commands/index.go
index c3f0aac32..9aa314e1f 100644
--- a/internal/commands/index.go
+++ b/internal/commands/index.go
@@ -13,8 +13,8 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// IndexCommand registers the index cli command.
@@ -57,9 +57,9 @@ func indexAction(ctx *cli.Context) error {
subPath := strings.TrimSpace(ctx.Args().First())
if subPath == "" {
- log.Infof("indexing originals in %s", sanitize.Log(conf.OriginalsPath()))
+ log.Infof("indexing originals in %s", clean.Log(conf.OriginalsPath()))
} else {
- log.Infof("indexing originals in %s", sanitize.Log(filepath.Join(conf.OriginalsPath(), subPath)))
+ log.Infof("indexing originals in %s", clean.Log(filepath.Join(conf.OriginalsPath(), subPath)))
}
if conf.ReadOnly() {
diff --git a/internal/commands/passwd.go b/internal/commands/passwd.go
index e214e4709..ebda39800 100644
--- a/internal/commands/passwd.go
+++ b/internal/commands/passwd.go
@@ -14,7 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// PasswdCommand updates a password.
@@ -39,7 +39,7 @@ func passwdAction(ctx *cli.Context) error {
user := entity.Admin
- log.Infof("please enter a new password for %s (at least 6 characters)\n", sanitize.Log(user.Username()))
+ log.Infof("please enter a new password for %s (at least 6 characters)\n", clean.Log(user.Username()))
newPassword := getPassword("New Password: ")
@@ -57,7 +57,7 @@ func passwdAction(ctx *cli.Context) error {
return err
}
- log.Infof("changed password for %s\n", sanitize.Log(user.Username()))
+ log.Infof("changed password for %s\n", clean.Log(user.Username()))
conf.Shutdown()
diff --git a/internal/commands/purge.go b/internal/commands/purge.go
index d0786dbf7..a0020371d 100644
--- a/internal/commands/purge.go
+++ b/internal/commands/purge.go
@@ -13,8 +13,8 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// PurgeCommand registers the index cli command.
@@ -56,9 +56,9 @@ func purgeAction(ctx *cli.Context) error {
subPath := strings.TrimSpace(ctx.Args().First())
if subPath == "" {
- log.Infof("purge: removing missing files in %s", sanitize.Log(filepath.Base(conf.OriginalsPath())))
+ log.Infof("purge: removing missing files in %s", clean.Log(filepath.Base(conf.OriginalsPath())))
} else {
- log.Infof("purge: removing missing files in %s", sanitize.Log(fs.RelName(filepath.Join(conf.OriginalsPath(), subPath), filepath.Dir(conf.OriginalsPath()))))
+ log.Infof("purge: removing missing files in %s", clean.Log(fs.RelName(filepath.Join(conf.OriginalsPath(), subPath), filepath.Dir(conf.OriginalsPath()))))
}
if conf.ReadOnly() {
diff --git a/internal/commands/restore.go b/internal/commands/restore.go
index b95a34c82..073590688 100644
--- a/internal/commands/restore.go
+++ b/internal/commands/restore.go
@@ -18,8 +18,8 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
const restoreDescription = "A user-defined SQL dump FILENAME can be passed as the first argument. " +
@@ -124,7 +124,7 @@ func restoreAction(ctx *cli.Context) error {
log.Warnf("replacing existing index with %d photos", counts.Photos)
}
- log.Infof("restoring index from %s", sanitize.Log(indexFileName))
+ log.Infof("restoring index from %s", clean.Log(indexFileName))
sqlBackup, err := os.ReadFile(indexFileName)
@@ -203,9 +203,9 @@ func restoreAction(ctx *cli.Context) error {
}
if !fs.PathExists(albumsPath) {
- log.Warnf("album files path %s not found", sanitize.Log(albumsPath))
+ log.Warnf("album files path %s not found", clean.Log(albumsPath))
} else {
- log.Infof("restoring albums from %s", sanitize.Log(albumsPath))
+ log.Infof("restoring albums from %s", clean.Log(albumsPath))
if count, err := photoprism.RestoreAlbums(albumsPath, true); err != nil {
return err
diff --git a/internal/commands/show_formats.go b/internal/commands/show_formats.go
index cee8a14a3..dac01964f 100644
--- a/internal/commands/show_formats.go
+++ b/internal/commands/show_formats.go
@@ -6,6 +6,7 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/pkg/fs"
+ "github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/report"
)
@@ -15,8 +16,8 @@ var ShowFormatsCommand = cli.Command{
Usage: "Lists supported media and sidecar file formats",
Flags: []cli.Flag{
cli.BoolFlag{
- Name: "compact, c",
- Usage: "hide format descriptions to make the output more compact",
+ Name: "short, s",
+ Usage: "hides format descriptions",
},
cli.BoolFlag{
Name: "md, m",
@@ -28,8 +29,7 @@ var ShowFormatsCommand = cli.Command{
// showFormatsAction lists supported media and sidecar file formats.
func showFormatsAction(ctx *cli.Context) error {
- rows, cols := fs.Extensions.Formats(true).Report(!ctx.Bool("compact"), true, true)
-
+ rows, cols := media.Report(fs.Extensions.Types(true), !ctx.Bool("short"), true, true)
fmt.Println(report.Table(rows, cols, ctx.Bool("md")))
return nil
diff --git a/internal/commands/start.go b/internal/commands/start.go
index d3f125607..ea4717760 100644
--- a/internal/commands/start.go
+++ b/internal/commands/start.go
@@ -18,8 +18,8 @@ import (
"github.com/photoprism/photoprism/internal/server"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/workers"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// StartCommand registers the start cli command.
@@ -95,7 +95,7 @@ func startAction(ctx *cli.Context) error {
if child != nil {
if !fs.Overwrite(conf.PIDFilename(), []byte(strconv.Itoa(child.Pid))) {
- log.Fatalf("failed writing process id to %s", sanitize.Log(conf.PIDFilename()))
+ log.Fatalf("failed writing process id to %s", clean.Log(conf.PIDFilename()))
}
log.Infof("daemon started with process id %v\n", child.Pid)
diff --git a/internal/commands/stop.go b/internal/commands/stop.go
index 43bbfe10d..e63793a54 100644
--- a/internal/commands/stop.go
+++ b/internal/commands/stop.go
@@ -7,7 +7,7 @@ import (
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// StopCommand registers the stop cli command.
@@ -22,7 +22,7 @@ var StopCommand = cli.Command{
func stopAction(ctx *cli.Context) error {
conf := config.NewConfig(ctx)
- log.Infof("looking for pid in %s", sanitize.Log(conf.PIDFilename()))
+ log.Infof("looking for pid in %s", clean.Log(conf.PIDFilename()))
dcxt := new(daemon.Context)
dcxt.PidFileName = conf.PIDFilename()
diff --git a/internal/commands/thumbs.go b/internal/commands/thumbs.go
index 25ce66790..500d4f731 100644
--- a/internal/commands/thumbs.go
+++ b/internal/commands/thumbs.go
@@ -7,7 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// ThumbsCommand registers the resample cli command.
@@ -34,7 +34,7 @@ func thumbsAction(ctx *cli.Context) error {
return err
}
- log.Infof("creating thumbnails in %s", sanitize.Log(conf.ThumbPath()))
+ log.Infof("creating thumbnails in %s", clean.Log(conf.ThumbPath()))
rs := service.Resample()
diff --git a/internal/commands/users.go b/internal/commands/users.go
index fa0a1f10f..6a0714306 100644
--- a/internal/commands/users.go
+++ b/internal/commands/users.go
@@ -14,7 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/query"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// UsersCommand registers user management subcommands.
@@ -183,7 +183,7 @@ func usersDeleteAction(ctx *cli.Context) error {
}
actionPrompt := promptui.Prompt{
- Label: fmt.Sprintf("Delete %s?", sanitize.Log(userName)),
+ Label: fmt.Sprintf("Delete %s?", clean.Log(userName)),
IsConfirm: true,
}
@@ -193,7 +193,7 @@ func usersDeleteAction(ctx *cli.Context) error {
} else if err := m.Delete(); err != nil {
return err
} else {
- log.Infof("%s deleted", sanitize.Log(userName))
+ log.Infof("%s deleted", clean.Log(userName))
}
} else {
log.Infof("keeping user")
@@ -242,7 +242,7 @@ func usersUpdateAction(ctx *cli.Context) error {
if err != nil {
return err
}
- fmt.Printf("password successfully changed: %s\n", sanitize.Log(u.Username()))
+ fmt.Printf("password successfully changed: %s\n", clean.Log(u.Username()))
}
if ctx.IsSet("fullname") {
@@ -261,7 +261,7 @@ func usersUpdateAction(ctx *cli.Context) error {
return err
}
- fmt.Printf("user successfully updated: %s\n", sanitize.Log(u.Username()))
+ fmt.Printf("user successfully updated: %s\n", clean.Log(u.Username()))
return nil
})
diff --git a/internal/config/doc.go b/internal/config/about.go
similarity index 100%
rename from internal/config/doc.go
rename to internal/config/about.go
diff --git a/internal/config/client.go b/internal/config/client_config.go
similarity index 96%
rename from internal/config/client.go
rename to internal/config/client_config.go
index 46b1f3dcd..dfd53db28 100644
--- a/internal/config/client.go
+++ b/internal/config/client_config.go
@@ -6,7 +6,9 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
+
"github.com/photoprism/photoprism/pkg/colors"
+ "github.com/photoprism/photoprism/pkg/env"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -64,7 +66,7 @@ type ClientConfig struct {
Colors []map[string]string `json:"colors"`
Categories CategoryLabels `json:"categories"`
Clip int `json:"clip"`
- Server RuntimeInfo `json:"server"`
+ Server env.Resources `json:"server"`
}
// Years represents a list of years.
@@ -384,7 +386,7 @@ func (c *Config) UserConfig() ClientConfig {
PreviewToken: c.PreviewToken(),
ManifestUri: c.ClientManifestUri(),
Clip: txt.ClipDefault,
- Server: NewRuntimeInfo(),
+ Server: env.Info(),
}
c.Db().
@@ -409,12 +411,12 @@ func (c *Config) UserConfig() ClientConfig {
c.Db().
Table("photos").
- Select("SUM(photo_type = 'video' AND photo_quality >= 0 AND photo_private = 0) AS videos, " +
- "SUM(photo_type = 'live' AND photo_quality >= 0 AND photo_private = 0) AS live, " +
- "SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','animated') AND photo_private = 0 AND photo_quality >= 0) AS photos, " +
- "SUM(photo_type IN ('image','raw','live','animated') AND photo_quality < 3 AND photo_quality >= 0 AND photo_private = 0) AS review, " +
- "SUM(photo_favorite = 1 AND photo_private = 0 AND photo_quality >= 0) AS favorites, " +
- "SUM(photo_private = 1 AND photo_quality >= 0) AS private").
+ Select("SUM(photo_type = 'video' AND photo_quality > -1 AND photo_private = 0) AS videos, " +
+ "SUM(photo_type = 'live' AND photo_quality > -1 AND photo_private = 0) AS live, " +
+ "SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','animated') AND photo_private = 0 AND photo_quality > -1) AS photos, " +
+ "SUM(photo_type IN ('image','raw','live','animated') AND photo_quality < 3 AND photo_quality > -1 AND photo_private = 0) AS review, " +
+ "SUM(photo_favorite = 1 AND photo_private = 0 AND photo_quality > -1) AS favorites, " +
+ "SUM(photo_private = 1 AND photo_quality > -1) AS private").
Where("photos.id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND (file_missing = 1 OR file_error <> ''))").
Where("deleted_at IS NULL").
Take(&result.Count)
@@ -460,7 +462,7 @@ func (c *Config) UserConfig() ClientConfig {
result.People, _ = query.People()
c.Db().
- Where("id IN (SELECT photos.camera_id FROM photos WHERE photos.photo_quality >= 0 OR photos.deleted_at IS NULL)").
+ Where("id IN (SELECT photos.camera_id FROM photos WHERE photos.photo_quality > -1 OR photos.deleted_at IS NULL)").
Where("deleted_at IS NULL").
Limit(10000).Order("camera_slug").
Find(&result.Cameras)
@@ -477,7 +479,7 @@ func (c *Config) UserConfig() ClientConfig {
c.Db().
Table("photos").
- Where("photo_year > 0 AND (photos.photo_quality >= 0 OR photos.deleted_at IS NULL)").
+ Where("photo_year > 0 AND (photos.photo_quality > -1 OR photos.deleted_at IS NULL)").
Order("photo_year DESC").
Pluck("DISTINCT photo_year", &result.Years)
diff --git a/internal/config/client_test.go b/internal/config/client_config_test.go
similarity index 100%
rename from internal/config/client_test.go
rename to internal/config/client_config_test.go
diff --git a/internal/config/config.go b/internal/config/config.go
index 9ffc08ecd..4341a1660 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -29,9 +29,9 @@ import (
"github.com/photoprism/photoprism/internal/hub/places"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/thumb"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
var log = event.Log
@@ -39,37 +39,6 @@ var once sync.Once
var LowMem = false
var TotalMem uint64
-const MsgSponsor = "PhotoPrism® needs your support!"
-const SignUpURL = "https://docs.photoprism.app/funding/"
-const MsgSignUp = "Visit " + SignUpURL + " to learn more."
-const MsgSponsorCommand = "Since running this command puts additional load on our infrastructure," +
- " we unfortunately can only offer it to sponsors."
-
-const ApiUri = "/api/v1" // REST API
-const StaticUri = "/static" // Static Content
-
-const DefaultAutoIndexDelay = int(5 * 60) // 5 Minutes
-const DefaultAutoImportDelay = int(3 * 60) // 3 Minutes
-
-const DefaultWakeupIntervalSeconds = int(15 * 60) // 15 Minutes
-const DefaultWakeupInterval = time.Second * time.Duration(DefaultWakeupIntervalSeconds)
-const MaxWakeupInterval = time.Hour * 24 // 1 Day
-
-// Megabyte in bytes.
-const Megabyte = 1000 * 1000
-
-// Gigabyte in bytes.
-const Gigabyte = Megabyte * 1000
-
-// MinMem is the minimum amount of system memory required.
-const MinMem = Gigabyte
-
-// RecommendedMem is the recommended amount of system memory.
-const RecommendedMem = 3 * Gigabyte
-
-// serialName is the name of the unique storage serial.
-const serialName = "serial"
-
// Config holds database, cache and all parameters of photoprism
type Config struct {
once sync.Once
@@ -125,16 +94,16 @@ func NewConfig(ctx *cli.Context) *Config {
// Initialize options from config file and CLI context.
c := &Config{
options: NewOptions(ctx),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
env: os.Getenv("DOCKER_ENV"),
}
// Overwrite values with options.yml from config path.
if optionsYaml := c.OptionsYaml(); fs.FileExists(optionsYaml) {
if err := c.options.Load(optionsYaml); err != nil {
- log.Warnf("config: failed loading values from %s (%s)", sanitize.Log(optionsYaml), err)
+ log.Warnf("config: failed loading values from %s (%s)", clean.Log(optionsYaml), err)
} else {
- log.Debugf("config: overriding config with values from %s", sanitize.Log(optionsYaml))
+ log.Debugf("config: overriding config with values from %s", clean.Log(optionsYaml))
}
}
@@ -210,7 +179,7 @@ func (c *Config) Init() error {
}
if cpuName := cpuid.CPU.BrandName; cpuName != "" {
- log.Debugf("config: running on %s, %s memory detected", sanitize.Log(cpuid.CPU.BrandName), humanize.Bytes(TotalMem))
+ log.Debugf("config: running on %s, %s memory detected", clean.Log(cpuid.CPU.BrandName), humanize.Bytes(TotalMem))
}
// Exit if less than 128 MB RAM was detected.
@@ -261,7 +230,7 @@ func (c *Config) readSerial() string {
if data, err := os.ReadFile(storageName); err == nil && len(data) == 16 {
return string(data)
} else {
- log.Tracef("config: could not read %s (%s)", sanitize.Log(storageName), err)
+ log.Tracef("config: could not read %s (%s)", clean.Log(storageName), err)
}
}
@@ -269,7 +238,7 @@ func (c *Config) readSerial() string {
if data, err := os.ReadFile(backupName); err == nil && len(data) == 16 {
return string(data)
} else {
- log.Tracef("config: could not read %s (%s)", sanitize.Log(backupName), err)
+ log.Tracef("config: could not read %s (%s)", clean.Log(backupName), err)
}
}
@@ -282,7 +251,7 @@ func (c *Config) initSerial() (err error) {
return nil
}
- c.serial = rnd.PPID('z')
+ c.serial = rnd.GenerateUID('z')
storageName := filepath.Join(c.StoragePath(), serialName)
backupName := filepath.Join(c.BackupPath(), serialName)
diff --git a/internal/config/auth.go b/internal/config/config_auth.go
similarity index 96%
rename from internal/config/auth.go
rename to internal/config/config_auth.go
index 4a08bd66f..70ef55166 100644
--- a/internal/config/auth.go
+++ b/internal/config/config_auth.go
@@ -35,7 +35,7 @@ func (c *Config) InvalidDownloadToken(t string) bool {
// DownloadToken returns the DOWNLOAD api token (you can optionally use a static value for permanent caching).
func (c *Config) DownloadToken() string {
if c.options.DownloadToken == "" {
- c.options.DownloadToken = rnd.Token(8)
+ c.options.DownloadToken = rnd.GenerateToken(8)
}
return c.options.DownloadToken
diff --git a/internal/config/auth_test.go b/internal/config/config_auth_test.go
similarity index 100%
rename from internal/config/auth_test.go
rename to internal/config/config_auth_test.go
diff --git a/internal/config/config_const.go b/internal/config/config_const.go
new file mode 100644
index 000000000..98fe8ca93
--- /dev/null
+++ b/internal/config/config_const.go
@@ -0,0 +1,34 @@
+package config
+
+import "time"
+
+const MsgSponsor = "PhotoPrism® needs your support!"
+const SignUpURL = "https://docs.photoprism.app/funding/"
+const MsgSignUp = "Visit " + SignUpURL + " to learn more."
+const MsgSponsorCommand = "Since running this command puts additional load on our infrastructure," +
+ " we unfortunately can only offer it to sponsors."
+
+const ApiUri = "/api/v1" // REST API
+const StaticUri = "/static" // Static Content
+
+const DefaultAutoIndexDelay = int(5 * 60) // 5 Minutes
+const DefaultAutoImportDelay = int(3 * 60) // 3 Minutes
+
+const DefaultWakeupIntervalSeconds = int(15 * 60) // 15 Minutes
+const DefaultWakeupInterval = time.Second * time.Duration(DefaultWakeupIntervalSeconds)
+const MaxWakeupInterval = time.Hour * 24 // 1 Day
+
+// Megabyte in bytes.
+const Megabyte = 1000 * 1000
+
+// Gigabyte in bytes.
+const Gigabyte = Megabyte * 1000
+
+// MinMem is the minimum amount of system memory required.
+const MinMem = Gigabyte
+
+// RecommendedMem is the recommended amount of system memory.
+const RecommendedMem = 3 * Gigabyte
+
+// serialName is the name of the unique storage serial.
+const serialName = "serial"
diff --git a/internal/config/database.go b/internal/config/config_db.go
similarity index 100%
rename from internal/config/database.go
rename to internal/config/config_db.go
diff --git a/internal/config/database_test.go b/internal/config/config_db_test.go
similarity index 100%
rename from internal/config/database_test.go
rename to internal/config/config_db_test.go
diff --git a/internal/config/face.go b/internal/config/config_faces.go
similarity index 100%
rename from internal/config/face.go
rename to internal/config/config_faces.go
diff --git a/internal/config/face_test.go b/internal/config/config_faces_test.go
similarity index 100%
rename from internal/config/face_test.go
rename to internal/config/config_faces_test.go
diff --git a/internal/config/disable.go b/internal/config/config_features.go
similarity index 100%
rename from internal/config/disable.go
rename to internal/config/config_features.go
diff --git a/internal/config/disable_test.go b/internal/config/config_features_test.go
similarity index 100%
rename from internal/config/disable_test.go
rename to internal/config/config_features_test.go
diff --git a/internal/config/ffmpeg.go b/internal/config/config_ffmpeg.go
similarity index 100%
rename from internal/config/ffmpeg.go
rename to internal/config/config_ffmpeg.go
diff --git a/internal/config/ffmpeg_test.go b/internal/config/config_ffmpeg_test.go
similarity index 100%
rename from internal/config/ffmpeg_test.go
rename to internal/config/config_ffmpeg_test.go
diff --git a/internal/config/fs.go b/internal/config/config_filepaths.go
similarity index 97%
rename from internal/config/fs.go
rename to internal/config/config_filepaths.go
index 362b805b0..afd358cf6 100644
--- a/internal/config/fs.go
+++ b/internal/config/config_filepaths.go
@@ -7,8 +7,8 @@ import (
"os/user"
"path/filepath"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// binPaths stores known executable paths.
@@ -48,9 +48,9 @@ func findExecutable(configBin, defaultBin string) (binPath string) {
func (c *Config) CreateDirectories() error {
createError := func(path string, err error) (result error) {
if fs.FileExists(path) {
- result = fmt.Errorf("directory path %s is a file, please check your configuration", sanitize.Log(path))
+ result = fmt.Errorf("directory path %s is a file, please check your configuration", clean.Log(path))
} else {
- result = fmt.Errorf("failed to create the directory %s, check configuration and permissions", sanitize.Log(path))
+ result = fmt.Errorf("failed to create the directory %s, check configuration and permissions", clean.Log(path))
}
log.Debug(err)
@@ -59,7 +59,7 @@ func (c *Config) CreateDirectories() error {
}
notFoundError := func(name string) error {
- return fmt.Errorf("invalid %s path, check configuration and permissions", sanitize.Log(name))
+ return fmt.Errorf("invalid %s path, check configuration and permissions", clean.Log(name))
}
if c.AssetsPath() == "" {
@@ -154,11 +154,11 @@ func (c *Config) CreateDirectories() error {
if c.DarktableEnabled() {
if cachePath, err := c.CreateDarktableCachePath(); err != nil {
- return fmt.Errorf("could not create darktable cache path %s", sanitize.Log(cachePath))
+ return fmt.Errorf("could not create darktable cache path %s", clean.Log(cachePath))
}
if configPath, err := c.CreateDarktableConfigPath(); err != nil {
- return fmt.Errorf("could not create darktable cache path %s", sanitize.Log(configPath))
+ return fmt.Errorf("could not create darktable cache path %s", clean.Log(configPath))
}
}
diff --git a/internal/config/fs_test.go b/internal/config/config_filepaths_test.go
similarity index 96%
rename from internal/config/fs_test.go
rename to internal/config/config_filepaths_test.go
index 89efce0b0..0d79e265a 100644
--- a/internal/config/fs_test.go
+++ b/internal/config/config_filepaths_test.go
@@ -86,7 +86,7 @@ func TestConfig_CreateDirectories(t *testing.T) {
c := &Config{
options: NewTestOptions("config"),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
if err := c.CreateDirectories(); err != nil {
@@ -106,7 +106,7 @@ func TestConfig_CreateDirectories2(t *testing.T) {
defer testConfigMutex.Unlock()
c := &Config{
options: NewTestOptions(),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
c.options.AssetsPath = ""
@@ -130,7 +130,7 @@ func TestConfig_CreateDirectories2(t *testing.T) {
defer testConfigMutex.Unlock()
c := &Config{
options: NewTestOptions(),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
c.options.StoragePath = "/-*&^%$#@!`~"
@@ -147,7 +147,7 @@ func TestConfig_CreateDirectories2(t *testing.T) {
defer testConfigMutex.Unlock()
c := &Config{
options: NewTestOptions(),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
c.options.OriginalsPath = ""
@@ -172,7 +172,7 @@ func TestConfig_CreateDirectories2(t *testing.T) {
defer testConfigMutex.Unlock()
c := &Config{
options: NewTestOptions(),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
c.options.ImportPath = ""
@@ -197,7 +197,7 @@ func TestConfig_CreateDirectories2(t *testing.T) {
defer testConfigMutex.Unlock()
c := &Config{
options: NewTestOptions(),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
c.options.SidecarPath = "/-*&^%$#@!`~"
@@ -214,7 +214,7 @@ func TestConfig_CreateDirectories2(t *testing.T) {
defer testConfigMutex.Unlock()
c := &Config{
options: NewTestOptions(),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
c.options.CachePath = "/-*&^%$#@!`~"
@@ -231,7 +231,7 @@ func TestConfig_CreateDirectories2(t *testing.T) {
defer testConfigMutex.Unlock()
c := &Config{
options: NewTestOptions(),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
c.options.ConfigPath = "/-*&^%$#@!`~"
@@ -248,7 +248,7 @@ func TestConfig_CreateDirectories2(t *testing.T) {
defer testConfigMutex.Unlock()
c := &Config{
options: NewTestOptions(),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
c.options.TempPath = "/-*&^%$#@!`~"
diff --git a/internal/config/metadata.go b/internal/config/config_metadata.go
similarity index 100%
rename from internal/config/metadata.go
rename to internal/config/config_metadata.go
diff --git a/internal/config/metadata_test.go b/internal/config/config_metadata_test.go
similarity index 100%
rename from internal/config/metadata_test.go
rename to internal/config/config_metadata_test.go
diff --git a/internal/config/raw.go b/internal/config/config_raw.go
similarity index 100%
rename from internal/config/raw.go
rename to internal/config/config_raw.go
diff --git a/internal/config/raw_test.go b/internal/config/config_raw_test.go
similarity index 100%
rename from internal/config/raw_test.go
rename to internal/config/config_raw_test.go
diff --git a/internal/config/report.go b/internal/config/config_report.go
similarity index 100%
rename from internal/config/report.go
rename to internal/config/config_report.go
diff --git a/internal/config/resample.go b/internal/config/config_resample.go
similarity index 100%
rename from internal/config/resample.go
rename to internal/config/config_resample.go
diff --git a/internal/config/resample_test.go b/internal/config/config_resample_test.go
similarity index 100%
rename from internal/config/resample_test.go
rename to internal/config/config_resample_test.go
diff --git a/internal/config/server.go b/internal/config/config_server.go
similarity index 100%
rename from internal/config/server.go
rename to internal/config/config_server.go
diff --git a/internal/config/server_test.go b/internal/config/config_server_test.go
similarity index 100%
rename from internal/config/server_test.go
rename to internal/config/config_server_test.go
diff --git a/internal/config/tensorflow.go b/internal/config/config_tensorflow.go
similarity index 100%
rename from internal/config/tensorflow.go
rename to internal/config/config_tensorflow.go
diff --git a/internal/config/app.go b/internal/config/config_ui.go
similarity index 73%
rename from internal/config/app.go
rename to internal/config/config_ui.go
index 21a934842..9608ff04d 100644
--- a/internal/config/app.go
+++ b/internal/config/config_ui.go
@@ -4,10 +4,30 @@ import (
"path/filepath"
"strings"
+ "github.com/photoprism/photoprism/internal/i18n"
+
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
+// DefaultTheme returns the default user interface theme name.
+func (c *Config) DefaultTheme() string {
+ if c.options.DefaultTheme == "" || !c.Sponsor() {
+ return "default"
+ }
+
+ return c.options.DefaultTheme
+}
+
+// DefaultLocale returns the default user interface language locale name.
+func (c *Config) DefaultLocale() string {
+ if c.options.DefaultLocale == "" {
+ return i18n.Default.Locale()
+ }
+
+ return c.options.DefaultLocale
+}
+
// AppIcon returns the app icon when installed on a device.
func (c *Config) AppIcon() string {
defaultIcon := "logo"
diff --git a/internal/config/app_test.go b/internal/config/config_ui_test.go
similarity index 66%
rename from internal/config/app_test.go
rename to internal/config/config_ui_test.go
index d549c6f54..0bc9ceacb 100644
--- a/internal/config/app_test.go
+++ b/internal/config/config_ui_test.go
@@ -7,6 +7,31 @@ import (
"github.com/stretchr/testify/assert"
)
+func TestConfig_DefaultTheme(t *testing.T) {
+ c := NewConfig(CliTestContext())
+
+ assert.Equal(t, "default", c.DefaultTheme())
+ c.options.Sponsor = false
+ c.options.DefaultTheme = "grayscale"
+ assert.Equal(t, "default", c.DefaultTheme())
+ c.options.Sponsor = true
+ assert.Equal(t, "grayscale", c.DefaultTheme())
+ c.options.DefaultTheme = ""
+ assert.Equal(t, "default", c.DefaultTheme())
+ c.options.Sponsor = false
+ assert.Equal(t, "default", c.DefaultTheme())
+}
+
+func TestConfig_DefaultLocale(t *testing.T) {
+ c := NewConfig(CliTestContext())
+
+ assert.Equal(t, "en", c.DefaultLocale())
+ c.options.DefaultLocale = "de"
+ assert.Equal(t, "de", c.DefaultLocale())
+ c.options.DefaultLocale = ""
+ assert.Equal(t, "en", c.DefaultLocale())
+}
+
func TestConfig_AppIcon(t *testing.T) {
c := NewConfig(CliTestContext())
diff --git a/internal/config/default.go b/internal/config/default.go
deleted file mode 100644
index ef325d311..000000000
--- a/internal/config/default.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package config
-
-import (
- "github.com/photoprism/photoprism/internal/i18n"
-)
-
-// DefaultTheme returns the default user interface theme name.
-func (c *Config) DefaultTheme() string {
- if c.options.DefaultTheme == "" || !c.Sponsor() {
- return "default"
- }
-
- return c.options.DefaultTheme
-}
-
-// DefaultLocale returns the default user interface language locale name.
-func (c *Config) DefaultLocale() string {
- if c.options.DefaultLocale == "" {
- return i18n.Default.Locale()
- }
-
- return c.options.DefaultLocale
-}
diff --git a/internal/config/default_test.go b/internal/config/default_test.go
deleted file mode 100644
index 30f040e54..000000000
--- a/internal/config/default_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package config
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestConfig_DefaultTheme(t *testing.T) {
- c := NewConfig(CliTestContext())
-
- assert.Equal(t, "default", c.DefaultTheme())
- c.options.Sponsor = false
- c.options.DefaultTheme = "grayscale"
- assert.Equal(t, "default", c.DefaultTheme())
- c.options.Sponsor = true
- assert.Equal(t, "grayscale", c.DefaultTheme())
- c.options.DefaultTheme = ""
- assert.Equal(t, "default", c.DefaultTheme())
- c.options.Sponsor = false
- assert.Equal(t, "default", c.DefaultTheme())
-}
-
-func TestConfig_DefaultLocale(t *testing.T) {
- c := NewConfig(CliTestContext())
-
- assert.Equal(t, "en", c.DefaultLocale())
- c.options.DefaultLocale = "de"
- assert.Equal(t, "de", c.DefaultLocale())
- c.options.DefaultLocale = ""
- assert.Equal(t, "en", c.DefaultLocale())
-}
diff --git a/internal/config/errors.go b/internal/config/errors.go
deleted file mode 100644
index b61ffeb66..000000000
--- a/internal/config/errors.go
+++ /dev/null
@@ -1,18 +0,0 @@
-package config
-
-import (
- "errors"
-)
-
-// Define photoprism specific errors
-var (
- ErrReadOnly = errors.New("not available in read-only mode")
- ErrUnauthorized = errors.New("please log in and try again")
- ErrUploadNSFW = errors.New("upload might be offensive")
-)
-
-func LogError(err error) {
- if err != nil {
- log.Errorf("config: %s", err.Error())
- }
-}
diff --git a/internal/config/config_flags.go b/internal/config/global_flags.go
similarity index 100%
rename from internal/config/config_flags.go
rename to internal/config/global_flags.go
diff --git a/internal/config/config_options.go b/internal/config/global_options.go
similarity index 98%
rename from internal/config/config_options.go
rename to internal/config/global_options.go
index 43c08a694..e4a07f42b 100644
--- a/internal/config/config_options.go
+++ b/internal/config/global_options.go
@@ -10,8 +10,8 @@ import (
"github.com/urfave/cli"
"gopkg.in/yaml.v2"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -150,9 +150,9 @@ func NewOptions(ctx *cli.Context) *Options {
if defaultsYaml := ctx.GlobalString("defaults-yaml"); defaultsYaml == "" {
log.Tracef("config: defaults yaml file not specified")
} else if c.DefaultsYaml = fs.Abs(defaultsYaml); !fs.FileExists(c.DefaultsYaml) {
- log.Tracef("config: defaults file %s does not exist", sanitize.Log(c.DefaultsYaml))
+ log.Tracef("config: defaults file %s does not exist", clean.Log(c.DefaultsYaml))
} else if err := c.Load(c.DefaultsYaml); err != nil {
- log.Warnf("config: failed loading defaults from %s (%s)", sanitize.Log(c.DefaultsYaml), err)
+ log.Warnf("config: failed loading defaults from %s (%s)", clean.Log(c.DefaultsYaml), err)
}
if err := c.SetContext(ctx); err != nil {
diff --git a/internal/config/config_options_test.go b/internal/config/global_options_test.go
similarity index 100%
rename from internal/config/config_options_test.go
rename to internal/config/global_options_test.go
diff --git a/internal/config/log_error.go b/internal/config/log_error.go
new file mode 100644
index 000000000..85a1e252b
--- /dev/null
+++ b/internal/config/log_error.go
@@ -0,0 +1,15 @@
+package config
+
+import (
+ "errors"
+)
+
+var (
+ ErrReadOnly = errors.New("not available in read-only mode")
+)
+
+func LogError(err error) {
+ if err != nil {
+ log.Errorf("config: %s", err.Error())
+ }
+}
diff --git a/internal/config/settings.go b/internal/config/settings.go
index 86addb8e2..cab1a9e51 100644
--- a/internal/config/settings.go
+++ b/internal/config/settings.go
@@ -8,89 +8,10 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/i18n"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
-// UISettings represents user interface settings.
-type UISettings struct {
- Scrollbar bool `json:"scrollbar" yaml:"Scrollbar"`
- Zoom bool `json:"zoom" yaml:"Zoom"`
- Theme string `json:"theme" yaml:"Theme"`
- Language string `json:"language" yaml:"Language"`
-}
-
-// SearchSettings represents search UI preferences.
-type SearchSettings struct {
- BatchSize int `json:"batchSize" yaml:"BatchSize"`
-}
-
-// TemplateSettings represents template settings for the UI and messaging.
-type TemplateSettings struct {
- Default string `json:"default" yaml:"Default"`
-}
-
-// MapsSettings represents maps settings (for places).
-type MapsSettings struct {
- Animate int `json:"animate" yaml:"Animate"`
- Style string `json:"style" yaml:"Style"`
-}
-
-// FeatureSettings represents feature flags, mainly for the Web UI.
-type FeatureSettings struct {
- Upload bool `json:"upload" yaml:"Upload"`
- Download bool `json:"download" yaml:"Download"`
- Private bool `json:"private" yaml:"Private"`
- Review bool `json:"review" yaml:"Review"`
- Files bool `json:"files" yaml:"Files"`
- Videos bool `json:"videos" yaml:"Videos"`
- Folders bool `json:"folders" yaml:"Folders"`
- Albums bool `json:"albums" yaml:"Albums"`
- Moments bool `json:"moments" yaml:"Moments"`
- Estimates bool `json:"estimates" yaml:"Estimates"`
- People bool `json:"people" yaml:"People"`
- Labels bool `json:"labels" yaml:"Labels"`
- Places bool `json:"places" yaml:"Places"`
- Edit bool `json:"edit" yaml:"Edit"`
- Archive bool `json:"archive" yaml:"Archive"`
- Delete bool `json:"delete" yaml:"Delete"`
- Share bool `json:"share" yaml:"Share"`
- Library bool `json:"library" yaml:"Library"`
- Import bool `json:"import" yaml:"Import"`
- Logs bool `json:"logs" yaml:"Logs"`
-}
-
-// ImportSettings represents import settings.
-type ImportSettings struct {
- Path string `json:"path" yaml:"Path"`
- Move bool `json:"move" yaml:"Move"`
-}
-
-// IndexSettings represents indexing settings.
-type IndexSettings struct {
- Path string `json:"path" yaml:"Path"`
- Convert bool `json:"convert" yaml:"Convert"`
- Rescan bool `json:"rescan" yaml:"Rescan"`
-}
-
-// StackSettings represents settings for files that belong to the same photo.
-type StackSettings struct {
- UUID bool `json:"uuid" yaml:"UUID"`
- Meta bool `json:"meta" yaml:"Meta"`
- Name bool `json:"name" yaml:"Name"`
-}
-
-// ShareSettings represents content sharing settings.
-type ShareSettings struct {
- Title string `json:"title" yaml:"Title"`
-}
-
-// DownloadSettings represents content download settings.
-type DownloadSettings struct {
- Name entity.DownloadName `json:"name" yaml:"Name"`
- Raw bool `json:"raw" yaml:"Raw"`
-}
-
// Settings represents user settings for Web UI, indexing, and import.
type Settings struct {
UI UISettings `json:"ui" yaml:"UI"`
@@ -159,9 +80,7 @@ func NewSettings(c *Config) *Settings {
Share: ShareSettings{
Title: "",
},
- Download: DownloadSettings{
- Name: entity.DownloadNameDefault,
- },
+ Download: NewDownloadSettings(),
Templates: TemplateSettings{
Default: "index.tmpl",
},
@@ -191,7 +110,7 @@ func (s Settings) StackMeta() bool {
// Load user settings from file.
func (s *Settings) Load(fileName string) error {
if !fs.FileExists(fileName) {
- return fmt.Errorf("settings file not found: %s", sanitize.Log(fileName))
+ return fmt.Errorf("settings file not found: %s", clean.Log(fileName))
}
yamlConfig, err := os.ReadFile(fileName)
diff --git a/internal/config/settings_download.go b/internal/config/settings_download.go
new file mode 100644
index 000000000..1f629940c
--- /dev/null
+++ b/internal/config/settings_download.go
@@ -0,0 +1,23 @@
+package config
+
+import "github.com/photoprism/photoprism/internal/entity"
+
+// DownloadSettings represents content download settings.
+type DownloadSettings struct {
+ Name entity.DownloadName `json:"name" yaml:"Name"`
+ Disabled bool `json:"disabled" yaml:"Disabled"`
+ Originals bool `json:"originals" yaml:"Originals"`
+ MediaRaw bool `json:"mediaRaw" yaml:"MediaRaw"`
+ MediaSidecar bool `json:"mediaSidecar" yaml:"MediaSidecar"`
+}
+
+// NewDownloadSettings creates download settings with defaults.
+func NewDownloadSettings() DownloadSettings {
+ return DownloadSettings{
+ Name: entity.DownloadNameDefault,
+ Disabled: false,
+ Originals: true,
+ MediaRaw: false,
+ MediaSidecar: false,
+ }
+}
diff --git a/internal/config/settings_feature.go b/internal/config/settings_feature.go
new file mode 100644
index 000000000..acf318326
--- /dev/null
+++ b/internal/config/settings_feature.go
@@ -0,0 +1,25 @@
+package config
+
+// FeatureSettings represents feature flags, mainly for the Web UI.
+type FeatureSettings struct {
+ Upload bool `json:"upload" yaml:"Upload"`
+ Download bool `json:"download" yaml:"Download"`
+ Private bool `json:"private" yaml:"Private"`
+ Review bool `json:"review" yaml:"Review"`
+ Files bool `json:"files" yaml:"Files"`
+ Videos bool `json:"videos" yaml:"Videos"`
+ Folders bool `json:"folders" yaml:"Folders"`
+ Albums bool `json:"albums" yaml:"Albums"`
+ Moments bool `json:"moments" yaml:"Moments"`
+ Estimates bool `json:"estimates" yaml:"Estimates"`
+ People bool `json:"people" yaml:"People"`
+ Labels bool `json:"labels" yaml:"Labels"`
+ Places bool `json:"places" yaml:"Places"`
+ Edit bool `json:"edit" yaml:"Edit"`
+ Archive bool `json:"archive" yaml:"Archive"`
+ Delete bool `json:"delete" yaml:"Delete"`
+ Share bool `json:"share" yaml:"Share"`
+ Library bool `json:"library" yaml:"Library"`
+ Import bool `json:"import" yaml:"Import"`
+ Logs bool `json:"logs" yaml:"Logs"`
+}
diff --git a/internal/config/settings_import.go b/internal/config/settings_import.go
new file mode 100644
index 000000000..61d1be0c2
--- /dev/null
+++ b/internal/config/settings_import.go
@@ -0,0 +1,7 @@
+package config
+
+// ImportSettings represents import settings.
+type ImportSettings struct {
+ Path string `json:"path" yaml:"Path"`
+ Move bool `json:"move" yaml:"Move"`
+}
diff --git a/internal/config/settings_index.go b/internal/config/settings_index.go
new file mode 100644
index 000000000..dde24de0b
--- /dev/null
+++ b/internal/config/settings_index.go
@@ -0,0 +1,8 @@
+package config
+
+// IndexSettings represents indexing settings.
+type IndexSettings struct {
+ Path string `json:"path" yaml:"Path"`
+ Convert bool `json:"convert" yaml:"Convert"`
+ Rescan bool `json:"rescan" yaml:"Rescan"`
+}
diff --git a/internal/config/settings_maps.go b/internal/config/settings_maps.go
new file mode 100644
index 000000000..3fc1e7822
--- /dev/null
+++ b/internal/config/settings_maps.go
@@ -0,0 +1,7 @@
+package config
+
+// MapsSettings represents maps settings (for places).
+type MapsSettings struct {
+ Animate int `json:"animate" yaml:"Animate"`
+ Style string `json:"style" yaml:"Style"`
+}
diff --git a/internal/config/settings_search.go b/internal/config/settings_search.go
new file mode 100644
index 000000000..6c4f5c19e
--- /dev/null
+++ b/internal/config/settings_search.go
@@ -0,0 +1,6 @@
+package config
+
+// SearchSettings represents search UI preferences.
+type SearchSettings struct {
+ BatchSize int `json:"batchSize" yaml:"BatchSize"`
+}
diff --git a/internal/config/settings_share.go b/internal/config/settings_share.go
new file mode 100644
index 000000000..3510f7a5d
--- /dev/null
+++ b/internal/config/settings_share.go
@@ -0,0 +1,6 @@
+package config
+
+// ShareSettings represents content sharing settings.
+type ShareSettings struct {
+ Title string `json:"title" yaml:"Title"`
+}
diff --git a/internal/config/settings_stack.go b/internal/config/settings_stack.go
new file mode 100644
index 000000000..c652d3b91
--- /dev/null
+++ b/internal/config/settings_stack.go
@@ -0,0 +1,8 @@
+package config
+
+// StackSettings represents settings for files that belong to the same photo.
+type StackSettings struct {
+ UUID bool `json:"uuid" yaml:"UUID"`
+ Meta bool `json:"meta" yaml:"Meta"`
+ Name bool `json:"name" yaml:"Name"`
+}
diff --git a/internal/config/settings_template.go b/internal/config/settings_template.go
new file mode 100644
index 000000000..1247caffa
--- /dev/null
+++ b/internal/config/settings_template.go
@@ -0,0 +1,6 @@
+package config
+
+// TemplateSettings represents template settings for the UI and messaging.
+type TemplateSettings struct {
+ Default string `json:"default" yaml:"Default"`
+}
diff --git a/internal/config/settings_ui.go b/internal/config/settings_ui.go
new file mode 100644
index 000000000..446f0a71d
--- /dev/null
+++ b/internal/config/settings_ui.go
@@ -0,0 +1,9 @@
+package config
+
+// UISettings represents user interface settings.
+type UISettings struct {
+ Scrollbar bool `json:"scrollbar" yaml:"Scrollbar"`
+ Zoom bool `json:"zoom" yaml:"Zoom"`
+ Theme string `json:"theme" yaml:"Theme"`
+ Language string `json:"language" yaml:"Language"`
+}
diff --git a/internal/config/test.go b/internal/config/test.go
index 2d48e7e80..2719488d9 100644
--- a/internal/config/test.go
+++ b/internal/config/test.go
@@ -18,9 +18,9 @@ import (
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/capture"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Download URL and ZIP hash for test files.
@@ -69,7 +69,7 @@ func NewTestOptions(pkg string) *Options {
} else if dsn != SQLiteTestDB {
// Continue.
} else if err := os.Remove(dsn); err == nil {
- log.Debugf("sqlite: test file %s removed", sanitize.Log(dsn))
+ log.Debugf("sqlite: test file %s removed", clean.Log(dsn))
}
}
@@ -148,7 +148,7 @@ func NewTestConfig(pkg string) *Config {
c := &Config{
options: NewTestOptions(pkg),
- token: rnd.Token(8),
+ token: rnd.GenerateToken(8),
}
s := NewSettings(c)
diff --git a/internal/config/testdata/settings.yml b/internal/config/testdata/settings.yml
index bc349db69..7af7b36ad 100755
--- a/internal/config/testdata/settings.yml
+++ b/internal/config/testdata/settings.yml
@@ -44,6 +44,9 @@ Share:
Title: ""
Download:
Name: file
- Raw: false
+ Disabled: false
+ Originals: true
+ MediaRaw: false
+ MediaSidecar: false
Templates:
Default: index.tmpl
diff --git a/internal/config/thumbs.go b/internal/config/thumbnails.go
similarity index 100%
rename from internal/config/thumbs.go
rename to internal/config/thumbnails.go
diff --git a/internal/crop/cache.go b/internal/crop/cache.go
index cfbec213b..0bfe22a7d 100644
--- a/internal/crop/cache.go
+++ b/internal/crop/cache.go
@@ -6,8 +6,8 @@ import (
"path"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// FromCache returns the crop file name if cached.
@@ -28,7 +28,7 @@ func FromCache(hash, area string, size Size, thumbPath string) (fileName string,
// FileName returns the crop file name based on cache path, size, and area.
func FileName(hash, area string, width, height int, thumbPath string) (fileName string, err error) {
if len(hash) < 4 {
- return "", fmt.Errorf("crop: invalid file hash %s", sanitize.Log(hash))
+ return "", fmt.Errorf("crop: invalid file hash %s", clean.Log(hash))
}
if len(thumbPath) < 1 {
@@ -39,7 +39,7 @@ func FileName(hash, area string, width, height int, thumbPath string) (fileName
return "", fmt.Errorf("crop: invalid size %dx%d", width, height)
}
- fileName = path.Join(thumbPath, hash[0:1], hash[1:2], hash[2:3], fmt.Sprintf("%s_%dx%d_crop_%s%s", hash, width, height, area, fs.JpegExt))
+ fileName = path.Join(thumbPath, hash[0:1], hash[1:2], hash[2:3], fmt.Sprintf("%s_%dx%d_crop_%s%s", hash, width, height, area, fs.ExtJPEG))
return fileName, nil
}
diff --git a/internal/crop/image.go b/internal/crop/image.go
index ec1de15b6..e53ce3dbc 100644
--- a/internal/crop/image.go
+++ b/internal/crop/image.go
@@ -11,8 +11,8 @@ import (
"github.com/disintegration/imaging"
"github.com/photoprism/photoprism/internal/thumb"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Filenames of usable thumb sizes.
@@ -44,7 +44,7 @@ func ImageFromThumb(thumbName string, area Area, size Size, cache bool) (img ima
hash := thumbHash(thumbName)
// Compose cached crop image file name.
- cropBase := fmt.Sprintf("%s_%dx%d_crop_%s%s", hash, size.Width, size.Height, area.String(), fs.JpegExt)
+ cropBase := fmt.Sprintf("%s_%dx%d_crop_%s%s", hash, size.Width, size.Height, area.String(), fs.ExtJPEG)
cropName := filepath.Join(filePath, cropBase)
// Cached?
@@ -91,7 +91,7 @@ func ImageFromThumb(thumbName string, area Area, size Size, cache bool) (img ima
// ThumbFileName returns the ideal thumb file name.
func ThumbFileName(hash string, area Area, size Size, thumbPath string) (string, error) {
if len(hash) < 4 {
- return "", fmt.Errorf("invalid file hash %s", sanitize.Log(hash))
+ return "", fmt.Errorf("invalid file hash %s", clean.Log(hash))
}
if len(thumbPath) < 1 {
diff --git a/internal/crop/names.go b/internal/crop/names.go
index f084d60db..23a1cc884 100644
--- a/internal/crop/names.go
+++ b/internal/crop/names.go
@@ -7,7 +7,7 @@ type Name string
// Jpeg returns the crop name with a jpeg file extension suffix as string.
func (n Name) Jpeg() string {
- return string(n) + fs.JpegExt
+ return string(n) + fs.ExtJPEG
}
// Names of standard crop sizes.
diff --git a/internal/crop/request.go b/internal/crop/request.go
index 7ff740a53..c00e35393 100644
--- a/internal/crop/request.go
+++ b/internal/crop/request.go
@@ -27,7 +27,7 @@ func FromRequest(hash, area string, size Size, thumbPath string) (fileName strin
}
// Compose cached crop image file name.
- cropBase := fmt.Sprintf("%s_%dx%d_crop_%s%s", hash, size.Width, size.Height, area, fs.JpegExt)
+ cropBase := fmt.Sprintf("%s_%dx%d_crop_%s%s", hash, size.Width, size.Height, area, fs.ExtJPEG)
cropName := filepath.Join(filepath.Dir(thumbName), cropBase)
imageBuffer, err := os.ReadFile(thumbName)
diff --git a/internal/entity/address.go b/internal/entity/address.go
index 593bca9c4..d23d8468d 100644
--- a/internal/entity/address.go
+++ b/internal/entity/address.go
@@ -4,7 +4,7 @@ import (
"fmt"
"time"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
type Addresses []Address
@@ -64,7 +64,7 @@ func FirstOrCreateAddress(m *Address) *Address {
// String returns an identifier that can be used in logs.
func (m *Address) String() string {
- return sanitize.Log(fmt.Sprintf("%s, %s %s, %s", m.AddressLine1, m.AddressZip, m.AddressCity, m.AddressCountry))
+ return clean.Log(fmt.Sprintf("%s, %s %s, %s", m.AddressLine1, m.AddressZip, m.AddressCity, m.AddressCountry))
}
// Unknown returns true if the address is unknown.
diff --git a/internal/entity/album.go b/internal/entity/album.go
index c7ac0c6ce..2e328a490 100644
--- a/internal/entity/album.go
+++ b/internal/entity/album.go
@@ -13,8 +13,8 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/maps"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -72,7 +72,7 @@ func AddPhotoToAlbums(photo string, albums []string) (err error) {
return nil
}
- if !rnd.IsPPID(photo, 'p') {
+ if !rnd.EntityUID(photo, 'p') {
return fmt.Errorf("album: invalid photo uid %s", photo)
}
@@ -84,7 +84,7 @@ func AddPhotoToAlbums(photo string, albums []string) (err error) {
continue
}
- if rnd.IsPPID(album, 'a') {
+ if rnd.EntityUID(album, 'a') {
aUID = album
} else {
a := NewAlbum(album, AlbumDefault)
@@ -297,7 +297,7 @@ func FindFolderAlbum(albumPath string) *Album {
// Find returns an entity from the database.
func (m *Album) Find() (err error) {
- if rnd.IsPPID(m.AlbumUID, 'a') {
+ if rnd.EntityUID(m.AlbumUID, 'a') {
if err := UnscopedDb().First(m, "album_uid = ?", m.AlbumUID).Error; err != nil {
return err
}
@@ -326,25 +326,25 @@ func (m *Album) Find() (err error) {
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Album) BeforeCreate(scope *gorm.Scope) error {
- if rnd.IsUID(m.AlbumUID, 'a') {
+ if rnd.ValidID(m.AlbumUID, 'a') {
return nil
}
- return scope.SetColumn("AlbumUID", rnd.PPID('a'))
+ return scope.SetColumn("AlbumUID", rnd.GenerateUID('a'))
}
// String returns the id or name as string.
func (m *Album) String() string {
if m.AlbumSlug != "" {
- return sanitize.Log(m.AlbumSlug)
+ return clean.Log(m.AlbumSlug)
}
if m.AlbumTitle != "" {
- return sanitize.Log(m.AlbumTitle)
+ return clean.Log(m.AlbumTitle)
}
if m.AlbumUID != "" {
- return sanitize.Log(m.AlbumUID)
+ return clean.Log(m.AlbumUID)
}
return "[unknown album]"
diff --git a/internal/entity/album_yaml.go b/internal/entity/album_yaml.go
index c4ae56eb7..63b47a455 100644
--- a/internal/entity/album_yaml.go
+++ b/internal/entity/album_yaml.go
@@ -62,5 +62,5 @@ func (m *Album) LoadFromYaml(fileName string) error {
// YamlFileName returns the YAML file name.
func (m *Album) YamlFileName(albumsPath string) string {
- return filepath.Join(albumsPath, m.AlbumType, m.AlbumUID+fs.YamlExt)
+ return filepath.Join(albumsPath, m.AlbumType, m.AlbumUID+fs.ExtYAML)
}
diff --git a/internal/entity/camera.go b/internal/entity/camera.go
index 83cc35e1a..9d336aede 100644
--- a/internal/entity/camera.go
+++ b/internal/entity/camera.go
@@ -6,7 +6,7 @@ import (
"time"
"github.com/photoprism/photoprism/internal/event"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -129,7 +129,7 @@ func FirstOrCreateCamera(m *Camera) *Camera {
cameraCache.SetDefault(m.CameraSlug, &result)
return &result
} else {
- log.Errorf("camera: %s (create %s)", err.Error(), sanitize.Log(m.String()))
+ log.Errorf("camera: %s (create %s)", err.Error(), clean.Log(m.String()))
}
return &UnknownCamera
@@ -137,7 +137,7 @@ func FirstOrCreateCamera(m *Camera) *Camera {
// String returns an identifier that can be used in logs.
func (m *Camera) String() string {
- return sanitize.Log(m.CameraName)
+ return clean.Log(m.CameraName)
}
// Unknown returns true if the camera is not a known make or model.
diff --git a/internal/entity/duplicate.go b/internal/entity/duplicate.go
index 427a8e197..844063ea7 100644
--- a/internal/entity/duplicate.go
+++ b/internal/entity/duplicate.go
@@ -3,7 +3,7 @@ package entity
import (
"fmt"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
type Duplicates []Duplicate
@@ -56,7 +56,7 @@ func PurgeDuplicate(fileName, fileRoot string) error {
}
if err := UnscopedDb().Delete(Duplicate{}, "file_name = ? AND file_root = ?", fileName, fileRoot).Error; err != nil {
- log.Errorf("duplicate: %s in %s (purge)", err, sanitize.Log(fileName))
+ log.Errorf("duplicate: %s in %s (purge)", err, clean.Log(fileName))
return err
}
@@ -101,7 +101,7 @@ func (m *Duplicate) Save() error {
}
if err := UnscopedDb().Save(m).Error; err != nil {
- log.Errorf("duplicate: %s in %s (save)", err, sanitize.Log(m.FileName))
+ log.Errorf("duplicate: %s in %s (save)", err, clean.Log(m.FileName))
return err
}
diff --git a/internal/entity/const.go b/internal/entity/entity_const.go
similarity index 56%
rename from internal/entity/const.go
rename to internal/entity/entity_const.go
index 4b1aaac3b..79f717fe3 100644
--- a/internal/entity/const.go
+++ b/internal/entity/entity_const.go
@@ -1,32 +1,33 @@
package entity
-import "github.com/photoprism/photoprism/pkg/fs"
-
-// Panorama Projection Types
-// TODO: Move to separate package.
-
-const (
- ProjDefault = ""
- ProjEquirectangular = "equirectangular"
- ProjCubestrip = "cubestrip"
- ProjCylindrical = "cylindrical"
- ProjTransverseCylindrical = "transverse-cylindrical"
- ProjPseudocylindricalCompromise = "pseudocylindrical-compromise"
+import (
+ "github.com/photoprism/photoprism/pkg/media"
)
-// Media Types.
+// Default values.
+const (
+ Unknown = ""
+ UnknownYear = -1
+ UnknownMonth = -1
+ UnknownDay = -1
+ UnknownName = "Unknown"
+ UnknownTitle = UnknownName
+ UnknownID = "zz"
+)
+
+// Media content types.
const (
MediaUnknown = ""
- MediaImage = string(fs.MediaImage)
- MediaVector = string(fs.MediaVector)
- MediaAnimated = "animated"
- MediaLive = "live"
- MediaVideo = string(fs.MediaVideo)
- MediaRaw = string(fs.MediaRaw)
- TypeMeta = "meta"
+ MediaImage = string(media.Image)
+ MediaVector = string(media.Vector)
+ MediaAnimated = string(media.Animated)
+ MediaLive = string(media.Live)
+ MediaVideo = string(media.Video)
+ MediaRaw = string(media.Raw)
+ MediaText = string(media.Text)
)
-// Root Dirs.
+// Storage root folders.
const (
RootUnknown = ""
RootOriginals = "/"
@@ -36,34 +37,21 @@ const (
RootPath = "/"
)
-// Defaults.
+// Event type.
const (
- UnknownYear = -1
- UnknownMonth = -1
- UnknownDay = -1
- UnknownName = "Unknown"
- UnknownTitle = UnknownName
- UnknownID = "zz"
-)
-
-// Event Types
-
-const (
- Updated = "updated"
Created = "created"
+ Updated = "updated"
Deleted = "deleted"
)
-// Photo Stacks
-
+// Photo stack states.
const (
IsStacked int8 = 1
IsStackable int8 = 0
IsUnstacked int8 = -1
)
-// Sort Orders
-
+// Sort options.
const (
SortOrderDefault = ""
SortOrderRelevance = "relevance"
diff --git a/internal/entity/entity_count.go b/internal/entity/entity_count.go
index 73dbd325a..87e4e8d06 100644
--- a/internal/entity/entity_count.go
+++ b/internal/entity/entity_count.go
@@ -1,14 +1,7 @@
package entity
import (
- "fmt"
- "strings"
- "time"
-
- "github.com/dustin/go-humanize/english"
"github.com/jinzhu/gorm"
-
- "github.com/photoprism/photoprism/internal/mutex"
)
// Count returns the number of records for a given a model and key values.
@@ -33,227 +26,3 @@ func Count(m interface{}, keys []string, values []interface{}) int {
return count
}
-
-type LabelPhotoCount struct {
- LabelID int
- PhotoCount int
-}
-
-type LabelPhotoCounts []LabelPhotoCount
-
-// LabelCounts returns the number of photos for each label ID.
-func LabelCounts() LabelPhotoCounts {
- result := LabelPhotoCounts{}
-
- if err := UnscopedDb().Raw(`
- SELECT label_id, SUM(photo_count) AS photo_count FROM (
- SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
- JOIN photos_labels pl ON pl.label_id = l.id
- JOIN photos ph ON pl.photo_id = ph.id
- WHERE pl.uncertainty < 100
- AND ph.photo_quality >= 0
- AND ph.photo_private = 0
- AND ph.deleted_at IS NULL GROUP BY l.id
- UNION ALL
- SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
- JOIN categories c ON c.category_id = l.id
- JOIN photos_labels pl ON pl.label_id = c.label_id
- JOIN photos ph ON pl.photo_id = ph.id
- WHERE pl.uncertainty < 100
- AND ph.photo_quality >= 0
- AND ph.photo_private = 0
- AND ph.deleted_at IS NULL GROUP BY l.id) counts GROUP BY label_id
- `).Scan(&result).Error; err != nil {
- log.Errorf("label-count: %s", err.Error())
- }
-
- return result
-}
-
-// UpdatePlacesCounts updates the places photo counts.
-func UpdatePlacesCounts() (err error) {
- mutex.Index.Lock()
- defer mutex.Index.Unlock()
-
- start := time.Now()
-
- // Update places.
- res := Db().Table("places").
- UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos p "+
- "WHERE places.id = p.place_id "+
- "AND p.photo_quality >= 0 "+
- "AND p.photo_private = 0 "+
- "AND p.deleted_at IS NULL)"))
-
- if res.Error != nil {
- return res.Error
- }
-
- log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "place", "places"), time.Since(start))
-
- return nil
-}
-
-// UpdateSubjectCounts updates the subject file counts.
-func UpdateSubjectCounts() (err error) {
- mutex.Index.Lock()
- defer mutex.Index.Unlock()
-
- start := time.Now()
-
- var res *gorm.DB
-
- subjTable := Subject{}.TableName()
- filesTable := File{}.TableName()
- markerTable := Marker{}.TableName()
-
- condition := gorm.Expr("subj_type = ?", SubjPerson)
-
- switch DbDialect() {
- case MySQL:
- res = Db().Exec(`UPDATE ? LEFT JOIN (
- SELECT m.subj_uid, COUNT(DISTINCT f.id) AS subj_files, COUNT(DISTINCT f.photo_id) AS subj_photos FROM ? f
- JOIN ? m ON f.file_uid = m.file_uid AND m.subj_uid IS NOT NULL AND m.subj_uid <> '' AND m.subj_uid IS NOT NULL
- WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL GROUP BY m.subj_uid
- ) b ON b.subj_uid = subjects.subj_uid
- SET subjects.file_count = CASE WHEN b.subj_files IS NULL THEN 0 ELSE b.subj_files END,
- subjects.photo_count = CASE WHEN b.subj_photos IS NULL THEN 0 ELSE b.subj_photos END
- WHERE ?`, gorm.Expr(subjTable), gorm.Expr(filesTable), gorm.Expr(markerTable), condition)
- case SQLite3:
- // Update files count.
- res = Db().Table(subjTable).
- UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id) FROM files f "+
- fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ",
- markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition))
-
- // Update photo count.
- if res.Error != nil {
- return res.Error
- } else {
- photosRes := Db().Table(subjTable).
- UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(DISTINCT f.photo_id) FROM files f "+
- fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ",
- markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition))
- res.RowsAffected += photosRes.RowsAffected
- }
- default:
- return fmt.Errorf("sql: unsupported dialect %s", DbDialect())
- }
-
- if res.Error != nil {
- return res.Error
- }
-
- log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "subject", "subjects"), time.Since(start))
-
- return nil
-}
-
-// UpdateLabelCounts updates the label photo counts.
-func UpdateLabelCounts() (err error) {
- mutex.Index.Lock()
- defer mutex.Index.Unlock()
-
- start := time.Now()
- var res *gorm.DB
- if IsDialect(MySQL) {
- res = Db().Exec(`UPDATE labels LEFT JOIN (
- SELECT p2.label_id, COUNT(DISTINCT photo_id) AS label_photos FROM (
- SELECT pl.label_id as label_id, p.id AS photo_id FROM photos p
- JOIN photos_labels pl ON pl.photo_id = p.id AND pl.uncertainty < 100
- WHERE p.photo_quality > -1 AND p.photo_private = 0 AND p.deleted_at IS NULL
- UNION
- SELECT c.category_id as label_id, p.id AS photo_id FROM photos p
- JOIN photos_labels pl ON pl.photo_id = p.id AND pl.uncertainty < 100
- JOIN categories c ON c.label_id = pl.label_id
- WHERE p.photo_quality > -1 AND p.photo_private = 0 AND p.deleted_at IS NULL
- ) p2 GROUP BY p2.label_id
- ) b ON b.label_id = labels.id
- SET photo_count = CASE WHEN b.label_photos IS NULL THEN 0 ELSE b.label_photos END`)
- } else if IsDialect(SQLite3) {
- res = Db().
- Table("labels").
- UpdateColumn("photo_count",
- gorm.Expr(`(SELECT photo_count FROM (SELECT label_id, SUM(photo_count) AS photo_count FROM (
- SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
- JOIN photos_labels pl ON pl.label_id = l.id
- JOIN photos ph ON pl.photo_id = ph.id
- WHERE pl.uncertainty < 100
- AND ph.photo_quality >= 0
- AND ph.photo_private = 0
- AND ph.deleted_at IS NULL GROUP BY l.id
- UNION ALL
- SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
- JOIN categories c ON c.category_id = l.id
- JOIN photos_labels pl ON pl.label_id = c.label_id
- JOIN photos ph ON pl.photo_id = ph.id
- WHERE pl.uncertainty < 100
- AND ph.photo_quality >= 0
- AND ph.photo_private = 0
- AND ph.deleted_at IS NULL GROUP BY l.id) counts GROUP BY label_id) label_counts WHERE label_id = labels.id)`))
- } else {
- return fmt.Errorf("sql: unsupported dialect %s", DbDialect())
- }
-
- if res.Error != nil {
- return res.Error
- }
-
- log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "label", "labels"), time.Since(start))
-
- return nil
-}
-
-// UpdateCounts updates precalculated photo and file counts.
-func UpdateCounts() (err error) {
- log.Debug("index: updating counts")
-
- if err = UpdatePlacesCounts(); err != nil {
- if strings.Contains(err.Error(), "Error 1054") {
- log.Errorf("counts: failed updating places, potentially incompatible database version")
- log.Errorf("%s see https://jira.mariadb.org/browse/MDEV-25362", err)
- return nil
- }
-
- return err
- }
-
- if err = UpdateSubjectCounts(); err != nil {
- if strings.Contains(err.Error(), "Error 1054") {
- log.Errorf("counts: failed updating subjects, potentially incompatible database version")
- log.Errorf("%s see https://jira.mariadb.org/browse/MDEV-25362", err)
- return nil
- }
-
- return err
- }
-
- if err = UpdateLabelCounts(); err != nil {
- return err
- }
-
- /* TODO: Slow with many photos due to missing index.
- start = time.Now()
-
- // Update calendar album visibility.
- switch DbDialect() {
- default:
- if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = ? WHERE album_type=? AND id NOT IN (
- SELECT a.id FROM albums a JOIN photos p ON a.album_month = MONTH(p.taken_at) AND a.album_year = YEAR(p.taken_at)
- AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=? GROUP BY a.id)`,
- TimeStamp(), AlbumMonth, AlbumMonth).Error; err != nil {
- return err
- }
- if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = NULL WHERE album_type=? AND id IN (
- SELECT a.id FROM albums a JOIN photos p ON a.album_month = MONTH(p.taken_at) AND a.album_year = YEAR(p.taken_at)
- AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=? GROUP BY a.id)`,
- AlbumMonth, AlbumMonth).Error; err != nil {
- return err
- }
- }
-
- log.Debugf("calendar: updating visibility completed [%s]", time.Since(start))
- */
-
- return nil
-}
diff --git a/internal/entity/entity_count_test.go b/internal/entity/entity_count_test.go
index 7260e157e..c7cb86299 100644
--- a/internal/entity/entity_count_test.go
+++ b/internal/entity/entity_count_test.go
@@ -18,23 +18,3 @@ func TestCount(t *testing.T) {
assert.Equal(t, 1, result)
}
-
-func TestLabelCounts(t *testing.T) {
- results := LabelCounts()
-
- if len(results) == 0 {
- t.Fatal("at least one result expected")
- }
-
- for _, result := range results {
- t.Logf("LABEL COUNT: %+v", result)
- }
-}
-
-func TestUpdatePhotoCounts(t *testing.T) {
- err := UpdateCounts()
-
- if err != nil {
- t.Fatal(err)
- }
-}
diff --git a/internal/entity/entity_counts.go b/internal/entity/entity_counts.go
new file mode 100644
index 000000000..69411d246
--- /dev/null
+++ b/internal/entity/entity_counts.go
@@ -0,0 +1,236 @@
+package entity
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/dustin/go-humanize/english"
+ "github.com/jinzhu/gorm"
+
+ "github.com/photoprism/photoprism/internal/mutex"
+)
+
+type LabelPhotoCount struct {
+ LabelID int
+ PhotoCount int
+}
+
+type LabelPhotoCounts []LabelPhotoCount
+
+// LabelCounts returns the number of photos for each label ID.
+func LabelCounts() LabelPhotoCounts {
+ result := LabelPhotoCounts{}
+
+ if err := UnscopedDb().Raw(`
+ SELECT label_id, SUM(photo_count) AS photo_count FROM (
+ SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
+ JOIN photos_labels pl ON pl.label_id = l.id
+ JOIN photos ph ON pl.photo_id = ph.id
+ WHERE pl.uncertainty < 100
+ AND ph.photo_quality > -1
+ AND ph.photo_private = 0
+ AND ph.deleted_at IS NULL GROUP BY l.id
+ UNION ALL
+ SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
+ JOIN categories c ON c.category_id = l.id
+ JOIN photos_labels pl ON pl.label_id = c.label_id
+ JOIN photos ph ON pl.photo_id = ph.id
+ WHERE pl.uncertainty < 100
+ AND ph.photo_quality > -1
+ AND ph.photo_private = 0
+ AND ph.deleted_at IS NULL GROUP BY l.id) counts GROUP BY label_id
+ `).Scan(&result).Error; err != nil {
+ log.Errorf("label-count: %s", err.Error())
+ }
+
+ return result
+}
+
+// UpdatePlacesCounts updates the places photo counts.
+func UpdatePlacesCounts() (err error) {
+ mutex.Index.Lock()
+ defer mutex.Index.Unlock()
+
+ start := time.Now()
+
+ // Update places.
+ res := Db().Table("places").
+ UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(*) FROM photos p "+
+ "WHERE places.id = p.place_id "+
+ "AND p.photo_quality > -1 "+
+ "AND p.photo_private = 0 "+
+ "AND p.deleted_at IS NULL)"))
+
+ if res.Error != nil {
+ return res.Error
+ }
+
+ log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "place", "places"), time.Since(start))
+
+ return nil
+}
+
+// UpdateSubjectCounts updates the subject file counts.
+func UpdateSubjectCounts() (err error) {
+ mutex.Index.Lock()
+ defer mutex.Index.Unlock()
+
+ start := time.Now()
+
+ var res *gorm.DB
+
+ subjTable := Subject{}.TableName()
+ filesTable := File{}.TableName()
+ markerTable := Marker{}.TableName()
+
+ condition := gorm.Expr("subj_type = ?", SubjPerson)
+
+ switch DbDialect() {
+ case MySQL:
+ res = Db().Exec(`UPDATE ? LEFT JOIN (
+ SELECT m.subj_uid, COUNT(DISTINCT f.id) AS subj_files, COUNT(DISTINCT f.photo_id) AS subj_photos FROM ? f
+ JOIN ? m ON f.file_uid = m.file_uid AND m.subj_uid IS NOT NULL AND m.subj_uid <> '' AND m.subj_uid IS NOT NULL
+ WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL GROUP BY m.subj_uid
+ ) b ON b.subj_uid = subjects.subj_uid
+ SET subjects.file_count = CASE WHEN b.subj_files IS NULL THEN 0 ELSE b.subj_files END,
+ subjects.photo_count = CASE WHEN b.subj_photos IS NULL THEN 0 ELSE b.subj_photos END
+ WHERE ?`, gorm.Expr(subjTable), gorm.Expr(filesTable), gorm.Expr(markerTable), condition)
+ case SQLite3:
+ // Update files count.
+ res = Db().Table(subjTable).
+ UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id) FROM files f "+
+ fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ",
+ markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition))
+
+ // Update photo count.
+ if res.Error != nil {
+ return res.Error
+ } else {
+ photosRes := Db().Table(subjTable).
+ UpdateColumn("photo_count", gorm.Expr("(SELECT COUNT(DISTINCT f.photo_id) FROM files f "+
+ fmt.Sprintf("JOIN %s m ON f.file_uid = m.file_uid AND m.subj_uid = %s.subj_uid ",
+ markerTable, subjTable)+" WHERE m.marker_invalid = 0 AND f.deleted_at IS NULL) WHERE ?", condition))
+ res.RowsAffected += photosRes.RowsAffected
+ }
+ default:
+ return fmt.Errorf("sql: unsupported dialect %s", DbDialect())
+ }
+
+ if res.Error != nil {
+ return res.Error
+ }
+
+ log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "subject", "subjects"), time.Since(start))
+
+ return nil
+}
+
+// UpdateLabelCounts updates the label photo counts.
+func UpdateLabelCounts() (err error) {
+ mutex.Index.Lock()
+ defer mutex.Index.Unlock()
+
+ start := time.Now()
+ var res *gorm.DB
+ if IsDialect(MySQL) {
+ res = Db().Exec(`UPDATE labels LEFT JOIN (
+ SELECT p2.label_id, COUNT(DISTINCT photo_id) AS label_photos FROM (
+ SELECT pl.label_id as label_id, p.id AS photo_id FROM photos p
+ JOIN photos_labels pl ON pl.photo_id = p.id AND pl.uncertainty < 100
+ WHERE p.photo_quality > -1 AND p.photo_private = 0 AND p.deleted_at IS NULL
+ UNION
+ SELECT c.category_id as label_id, p.id AS photo_id FROM photos p
+ JOIN photos_labels pl ON pl.photo_id = p.id AND pl.uncertainty < 100
+ JOIN categories c ON c.label_id = pl.label_id
+ WHERE p.photo_quality > -1 AND p.photo_private = 0 AND p.deleted_at IS NULL
+ ) p2 GROUP BY p2.label_id
+ ) b ON b.label_id = labels.id
+ SET photo_count = CASE WHEN b.label_photos IS NULL THEN 0 ELSE b.label_photos END`)
+ } else if IsDialect(SQLite3) {
+ res = Db().
+ Table("labels").
+ UpdateColumn("photo_count",
+ gorm.Expr(`(SELECT photo_count FROM (SELECT label_id, SUM(photo_count) AS photo_count FROM (
+ SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
+ JOIN photos_labels pl ON pl.label_id = l.id
+ JOIN photos ph ON pl.photo_id = ph.id
+ WHERE pl.uncertainty < 100
+ AND ph.photo_quality > -1
+ AND ph.photo_private = 0
+ AND ph.deleted_at IS NULL GROUP BY l.id
+ UNION ALL
+ SELECT l.id AS label_id, COUNT(*) AS photo_count FROM labels l
+ JOIN categories c ON c.category_id = l.id
+ JOIN photos_labels pl ON pl.label_id = c.label_id
+ JOIN photos ph ON pl.photo_id = ph.id
+ WHERE pl.uncertainty < 100
+ AND ph.photo_quality > -1
+ AND ph.photo_private = 0
+ AND ph.deleted_at IS NULL GROUP BY l.id) counts GROUP BY label_id) label_counts WHERE label_id = labels.id)`))
+ } else {
+ return fmt.Errorf("sql: unsupported dialect %s", DbDialect())
+ }
+
+ if res.Error != nil {
+ return res.Error
+ }
+
+ log.Debugf("counts: updated %s [%s]", english.Plural(int(res.RowsAffected), "label", "labels"), time.Since(start))
+
+ return nil
+}
+
+// UpdateCounts updates precalculated photo and file counts.
+func UpdateCounts() (err error) {
+ log.Debug("index: updating counts")
+
+ if err = UpdatePlacesCounts(); err != nil {
+ if strings.Contains(err.Error(), "Error 1054") {
+ log.Errorf("counts: failed updating places, potentially incompatible database version")
+ log.Errorf("%s see https://jira.mariadb.org/browse/MDEV-25362", err)
+ return nil
+ }
+
+ return err
+ }
+
+ if err = UpdateSubjectCounts(); err != nil {
+ if strings.Contains(err.Error(), "Error 1054") {
+ log.Errorf("counts: failed updating subjects, potentially incompatible database version")
+ log.Errorf("%s see https://jira.mariadb.org/browse/MDEV-25362", err)
+ return nil
+ }
+
+ return err
+ }
+
+ if err = UpdateLabelCounts(); err != nil {
+ return err
+ }
+
+ /* TODO: Slow with many photos due to missing index.
+ start = time.Now()
+
+ // Update calendar album visibility.
+ switch DbDialect() {
+ default:
+ if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = ? WHERE album_type=? AND id NOT IN (
+ SELECT a.id FROM albums a JOIN photos p ON a.album_month = MONTH(p.taken_at) AND a.album_year = YEAR(p.taken_at)
+ AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=? GROUP BY a.id)`,
+ TimeStamp(), AlbumMonth, AlbumMonth).Error; err != nil {
+ return err
+ }
+ if err = UnscopedDb().Exec(`UPDATE albums SET deleted_at = NULL WHERE album_type=? AND id IN (
+ SELECT a.id FROM albums a JOIN photos p ON a.album_month = MONTH(p.taken_at) AND a.album_year = YEAR(p.taken_at)
+ AND p.deleted_at IS NULL AND p.photo_quality > -1 AND p.photo_private = 0 WHERE album_type=? GROUP BY a.id)`,
+ AlbumMonth, AlbumMonth).Error; err != nil {
+ return err
+ }
+ }
+
+ log.Debugf("calendar: updating visibility completed [%s]", time.Since(start))
+ */
+
+ return nil
+}
diff --git a/internal/entity/entity_counts_test.go b/internal/entity/entity_counts_test.go
new file mode 100644
index 000000000..4b9fa4819
--- /dev/null
+++ b/internal/entity/entity_counts_test.go
@@ -0,0 +1,25 @@
+package entity
+
+import (
+ "testing"
+)
+
+func TestLabelCounts(t *testing.T) {
+ results := LabelCounts()
+
+ if len(results) == 0 {
+ t.Fatal("at least one result expected")
+ }
+
+ for _, result := range results {
+ t.Logf("LABEL COUNT: %+v", result)
+ }
+}
+
+func TestUpdatePhotoCounts(t *testing.T) {
+ err := UpdateCounts()
+
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/internal/entity/entity_init.go b/internal/entity/entity_init.go
index 4894ad565..e19ab36b8 100644
--- a/internal/entity/entity_init.go
+++ b/internal/entity/entity_init.go
@@ -4,7 +4,7 @@ import (
"os"
"time"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// onReady contains init functions to be called when the
@@ -62,7 +62,7 @@ func InitTestDb(driver, dsn string) *Gorm {
} else if dsn != SQLiteTestDB {
// Continue.
} else if err := os.Remove(dsn); err == nil {
- log.Debugf("sqlite: test file %s removed", sanitize.Log(dsn))
+ log.Debugf("sqlite: test file %s removed", clean.Log(dsn))
}
}
diff --git a/internal/entity/entity_save_test.go b/internal/entity/entity_save_test.go
index 730ee39d6..692ca4be9 100644
--- a/internal/entity/entity_save_test.go
+++ b/internal/entity/entity_save_test.go
@@ -13,7 +13,7 @@ func TestSave(t *testing.T) {
t.Run("HasCreatedUpdatedAt", func(t *testing.T) {
id := 99999 + r.Intn(10000)
- m := Photo{ID: uint(id), PhotoUID: rnd.PPID('p'), UpdatedAt: TimeStamp(), CreatedAt: TimeStamp()}
+ m := Photo{ID: uint(id), PhotoUID: rnd.GenerateUID('p'), UpdatedAt: TimeStamp(), CreatedAt: TimeStamp()}
if err := m.Save(); err != nil {
t.Fatal(err)
@@ -27,7 +27,7 @@ func TestSave(t *testing.T) {
})
t.Run("HasCreatedAt", func(t *testing.T) {
id := 99999 + r.Intn(10000)
- m := Photo{ID: uint(id), PhotoUID: rnd.PPID('p'), CreatedAt: TimeStamp()}
+ m := Photo{ID: uint(id), PhotoUID: rnd.GenerateUID('p'), CreatedAt: TimeStamp()}
if err := m.Save(); err != nil {
t.Fatal(err)
@@ -41,7 +41,7 @@ func TestSave(t *testing.T) {
})
t.Run("NoCreatedAt", func(t *testing.T) {
id := 99999 + r.Intn(10000)
- m := Photo{ID: uint(id), PhotoUID: rnd.PPID('p'), CreatedAt: TimeStamp()}
+ m := Photo{ID: uint(id), PhotoUID: rnd.GenerateUID('p'), CreatedAt: TimeStamp()}
if err := m.Save(); err != nil {
t.Fatal(err)
diff --git a/internal/entity/entity_tables.go b/internal/entity/entity_tables.go
index 318b29528..834533e09 100644
--- a/internal/entity/entity_tables.go
+++ b/internal/entity/entity_tables.go
@@ -7,7 +7,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/migrate"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
type Tables map[string]interface{}
@@ -56,10 +56,10 @@ func (list Tables) WaitForMigration(db *gorm.DB) {
for i := 0; i <= attempts; i++ {
count := RowCount{}
if err := db.Raw(fmt.Sprintf("SELECT COUNT(*) AS count FROM %s", name)).Scan(&count).Error; err == nil {
- log.Tracef("migrate: %s migrated", sanitize.Log(name))
+ log.Tracef("migrate: %s migrated", clean.Log(name))
break
} else {
- log.Debugf("migrate: waiting for %s migration (%s)", sanitize.Log(name), err.Error())
+ log.Debugf("migrate: waiting for %s migration (%s)", clean.Log(name), err.Error())
}
if i == attempts {
@@ -78,7 +78,7 @@ func (list Tables) Truncate(db *gorm.DB) {
// log.Debugf("entity: removed all data from %s", name)
break
} else if err.Error() != "record not found" {
- log.Debugf("migrate: %s in %s", err, sanitize.Log(name))
+ log.Debugf("migrate: %s in %s", err, clean.Log(name))
}
}
}
@@ -93,7 +93,7 @@ func (list Tables) Migrate(db *gorm.DB, runFailed bool, ids []string) {
time.Sleep(time.Second)
if err := db.AutoMigrate(entity).Error; err != nil {
- log.Errorf("migrate: failed migrating %s", sanitize.Log(name))
+ log.Errorf("migrate: failed migrating %s", clean.Log(name))
panic(err)
}
}
diff --git a/internal/entity/entity_update_test.go b/internal/entity/entity_update_test.go
index 5175dfef7..3b4bc780b 100644
--- a/internal/entity/entity_update_test.go
+++ b/internal/entity/entity_update_test.go
@@ -13,7 +13,7 @@ import (
func TestUpdate(t *testing.T) {
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
t.Run("IDMissing", func(t *testing.T) {
- uid := rnd.PPID('p')
+ uid := rnd.GenerateUID('p')
m := &Photo{ID: 0, PhotoUID: uid, UpdatedAt: TimeStamp(), CreatedAt: TimeStamp(), PhotoTitle: "Foo"}
updatedAt := m.UpdatedAt
@@ -42,7 +42,7 @@ func TestUpdate(t *testing.T) {
})
t.Run("NotUpdated", func(t *testing.T) {
id := 99999 + r.Intn(10000)
- uid := rnd.PPID('p')
+ uid := rnd.GenerateUID('p')
m := &Photo{ID: uint(id), PhotoUID: uid, UpdatedAt: time.Now(), CreatedAt: TimeStamp(), PhotoTitle: "Foo"}
updatedAt := m.UpdatedAt
@@ -85,7 +85,7 @@ func TestUpdate(t *testing.T) {
t.Run("NonExistentKeys", func(t *testing.T) {
m := PhotoFixtures.Pointer("Photo01")
m.ID = uint(99999 + r.Intn(10000))
- m.PhotoUID = rnd.PPID('p')
+ m.PhotoUID = rnd.GenerateUID('p')
updatedAt := m.UpdatedAt
if err := Update(m, "ID", "PhotoUID"); err == nil {
t.Fatal("error expected")
diff --git a/internal/entity/face.go b/internal/entity/face.go
index 6e5c83174..2653e2a3d 100644
--- a/internal/entity/face.go
+++ b/internal/entity/face.go
@@ -411,7 +411,7 @@ func FindFace(id string) *Face {
// ValidFaceCount counts the number of valid face markers for a file uid.
func ValidFaceCount(fileUID string) (c int) {
- if !rnd.IsPPID(fileUID, 'f') {
+ if !rnd.EntityUID(fileUID, 'f') {
return
}
diff --git a/internal/entity/file.go b/internal/entity/file.go
index ef6e47c60..68ca9cb6b 100644
--- a/internal/entity/file.go
+++ b/internal/entity/file.go
@@ -5,7 +5,6 @@ import (
"math"
"path/filepath"
"sort"
- "strings"
"sync"
"time"
@@ -15,10 +14,14 @@ import (
"github.com/ulule/deepcopier"
"github.com/photoprism/photoprism/internal/face"
+
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs"
+ "github.com/photoprism/photoprism/pkg/media"
+ "github.com/photoprism/photoprism/pkg/projection"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/txt"
)
type DownloadName string
@@ -44,9 +47,9 @@ type File struct {
PhotoID uint `gorm:"index:idx_files_photo_id;" json:"-" yaml:"-"`
PhotoUID string `gorm:"type:VARBINARY(42);index;" json:"PhotoUID" yaml:"PhotoUID"`
PhotoTakenAt time.Time `gorm:"type:DATETIME;index;" json:"TakenAt" yaml:"TakenAt"`
- MetaUTC int64 `gorm:"column:meta_utc;index;" json:"MetaUTC" yaml:"MetaUTC,omitempty"`
TimeIndex *string `gorm:"type:VARBINARY(48);" json:"TimeIndex" yaml:"TimeIndex"`
MediaID *string `gorm:"type:VARBINARY(32);" json:"MediaID" yaml:"MediaID"`
+ MediaUTC int64 `gorm:"column:media_utc;index;" json:"MediaUTC" yaml:"MediaUTC,omitempty"`
InstanceID string `gorm:"type:VARBINARY(42);index;" json:"InstanceID,omitempty" yaml:"InstanceID,omitempty"`
FileUID string `gorm:"type:VARBINARY(42);unique_index;" json:"UID" yaml:"UID"`
FileName string `gorm:"type:VARBINARY(755);unique_index:idx_files_name_root;" json:"Name" yaml:"Name"`
@@ -187,11 +190,22 @@ func PrimaryFile(photoUID string) (*File, error) {
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *File) BeforeCreate(scope *gorm.Scope) error {
- if rnd.IsUID(m.FileUID, 'f') {
+ // Set MediaType based on FileName if empty.
+ if m.MediaType == "" && m.FileName != "" {
+ m.MediaType = media.FromName(m.FileName).String()
+ }
+
+ // Set MediaUTC based on PhotoTakenAt if empty.
+ if m.MediaUTC == 0 && !m.PhotoTakenAt.IsZero() {
+ m.MediaUTC = m.PhotoTakenAt.UnixMilli()
+ }
+
+ // Return if uid exists.
+ if rnd.ValidID(m.FileUID, 'f') {
return nil
}
- return scope.SetColumn("FileUID", rnd.PPID('f'))
+ return scope.SetColumn("FileUID", rnd.GenerateUID('f'))
}
// DownloadName returns the download file name.
@@ -248,7 +262,7 @@ func (m *File) ShareBase(seq int) string {
return fmt.Sprintf("%s.%s", m.FileHash, m.FileType)
}
- name := strings.Title(slug.MakeLang(photo.PhotoTitle, "en"))
+ name := txt.Title(slug.MakeLang(photo.PhotoTitle, "en"))
taken := photo.TakenAtLocal.Format("20060102-150405")
if seq > 0 {
@@ -281,23 +295,23 @@ func (m File) Missing() bool {
// DeletePermanently permanently removes a file from the index.
func (m *File) DeletePermanently() error {
if m.ID < 1 || m.FileUID == "" {
- return fmt.Errorf("invalid file id %d / uid %s", m.ID, sanitize.Log(m.FileUID))
+ return fmt.Errorf("invalid file id %d / uid %s", m.ID, clean.Log(m.FileUID))
}
if err := UnscopedDb().Delete(Marker{}, "file_uid = ?", m.FileUID).Error; err != nil {
- log.Errorf("file %s: %s while removing markers", sanitize.Log(m.FileUID), err)
+ log.Errorf("file %s: %s while removing markers", clean.Log(m.FileUID), err)
}
if err := UnscopedDb().Delete(FileShare{}, "file_id = ?", m.ID).Error; err != nil {
- log.Errorf("file %s: %s while removing share info", sanitize.Log(m.FileUID), err)
+ log.Errorf("file %s: %s while removing share info", clean.Log(m.FileUID), err)
}
if err := UnscopedDb().Delete(FileSync{}, "file_id = ?", m.ID).Error; err != nil {
- log.Errorf("file %s: %s while removing remote sync info", sanitize.Log(m.FileUID), err)
+ log.Errorf("file %s: %s while removing remote sync info", clean.Log(m.FileUID), err)
}
if err := m.ReplaceHash(""); err != nil {
- log.Errorf("file %s: %s while removing covers", sanitize.Log(m.FileUID), err)
+ log.Errorf("file %s: %s while removing covers", clean.Log(m.FileUID), err)
}
return UnscopedDb().Delete(m).Error
@@ -312,9 +326,9 @@ func (m *File) ReplaceHash(newHash string) error {
// Log values.
if m.FileHash != "" && newHash == "" {
- log.Tracef("file %s: removing hash %s", sanitize.Log(m.FileUID), sanitize.Log(m.FileHash))
+ log.Tracef("file %s: removing hash %s", clean.Log(m.FileUID), clean.Log(m.FileHash))
} else if m.FileHash != "" && newHash != "" {
- log.Tracef("file %s: hash %s changed to %s", sanitize.Log(m.FileUID), sanitize.Log(m.FileHash), sanitize.Log(newHash))
+ log.Tracef("file %s: hash %s changed to %s", clean.Log(m.FileUID), clean.Log(m.FileHash), clean.Log(newHash))
// Reset error when hash changes.
m.FileError = ""
}
@@ -350,7 +364,7 @@ func (m *File) ReplaceHash(newHash string) error {
// Delete deletes the entity from the database.
func (m *File) Delete(permanently bool) error {
if m.ID < 1 || m.FileUID == "" {
- return fmt.Errorf("invalid file id %d / uid %s", m.ID, sanitize.Log(m.FileUID))
+ return fmt.Errorf("invalid file id %d / uid %s", m.ID, clean.Log(m.FileUID))
}
if permanently {
@@ -401,7 +415,7 @@ func (m *File) Create() error {
}
if _, err := m.SaveMarkers(); err != nil {
- log.Errorf("file %s: %s while saving markers", sanitize.Log(m.FileUID), err)
+ log.Errorf("file %s: %s while saving markers", clean.Log(m.FileUID), err)
return err
}
@@ -434,12 +448,12 @@ func (m *File) Save() error {
}
if err := UnscopedDb().Save(m).Error; err != nil {
- log.Errorf("file %s: %s while saving", sanitize.Log(m.FileUID), err)
+ log.Errorf("file %s: %s while saving", clean.Log(m.FileUID), err)
return err
}
if _, err := m.SaveMarkers(); err != nil {
- log.Errorf("file %s: %s while saving markers", sanitize.Log(m.FileUID), err)
+ log.Errorf("file %s: %s while saving markers", clean.Log(m.FileUID), err)
return err
}
@@ -469,7 +483,7 @@ func (m *File) Updates(values interface{}) error {
// Rename updates the name and path of this file.
func (m *File) Rename(fileName, rootName, filePath, fileBase string) error {
- log.Debugf("file %s: renaming %s to %s", sanitize.Log(m.FileUID), sanitize.Log(m.FileName), sanitize.Log(fileName))
+ log.Debugf("file %s: renaming %s to %s", clean.Log(m.FileUID), clean.Log(m.FileName), clean.Log(fileName))
// Update database row.
if err := m.Updates(map[string]interface{}{
@@ -513,7 +527,7 @@ func (m *File) Undelete() error {
return err
}
- log.Debugf("file %s: removed missing flag from %s", sanitize.Log(m.FileUID), sanitize.Log(m.FileName))
+ log.Debugf("file %s: removed missing flag from %s", clean.Log(m.FileUID), clean.Log(m.FileName))
m.FileMissing = false
m.DeletedAt = nil
@@ -536,7 +550,7 @@ func (m *File) RelatedPhoto() *Photo {
// NoJPEG returns true if the file is not a JPEG image file.
func (m *File) NoJPEG() bool {
- return m.FileType != string(fs.FormatJpeg)
+ return fs.ImageJPEG.NotEqual(m.FileType)
}
// Links returns all share links for this entity.
@@ -549,7 +563,7 @@ func (m *File) Panorama() bool {
if m.FileSidecar || m.FileWidth <= 1000 || m.FileHeight <= 500 {
// Too small.
return false
- } else if m.Projection() != ProjDefault {
+ } else if m.Projection() != projection.Unknown {
// Panoramic projection.
return true
}
@@ -559,13 +573,17 @@ func (m *File) Panorama() bool {
}
// Projection returns the panorama projection name if any.
-func (m *File) Projection() string {
- return SanitizeStringTypeLower(m.FileProjection)
+func (m *File) Projection() projection.Type {
+ return projection.New(m.FileProjection)
}
// SetProjection sets the panorama projection name.
-func (m *File) SetProjection(name string) {
- m.FileProjection = SanitizeStringTypeLower(name)
+func (m *File) SetProjection(s string) {
+ if s == "" {
+ return
+ } else if t := projection.New(s); !t.Unknown() {
+ m.FileProjection = t.String()
+ }
}
// IsHDR returns true if it is a high dynamic range file.
@@ -592,7 +610,7 @@ func (m *File) HasWatermark() bool {
// IsAnimated returns true if the file has animated image frames.
func (m *File) IsAnimated() bool {
- return m.FileFrames > 1 && m.MediaType == MediaImage
+ return m.FileFrames > 1 && media.Image.Equal(m.MediaType)
}
// ColorProfile returns the ICC color profile name if any.
@@ -633,12 +651,12 @@ func (m *File) SetDuration(d time.Duration) {
m.FileDuration = d.Round(time.Second)
// Update number of frames.
- if m.FileFrames <= 0 && m.FileFPS > 0 {
+ if m.FileFrames == 0 && m.FileFPS > 1 {
m.FileFrames = int(math.Round(m.FileFPS * m.FileDuration.Seconds()))
}
// Update number of frames per second.
- if m.FileFPS <= 0 && m.FileFrames > 0 {
+ if m.FileFPS == 0 && m.FileFrames > 1 {
m.FileFPS = float64(m.FileFrames) / m.FileDuration.Seconds()
}
}
@@ -652,7 +670,7 @@ func (m *File) SetFPS(frameRate float64) {
m.FileFPS = frameRate
// Update number of frames.
- if m.FileFrames <= 0 && m.FileDuration > 0 {
+ if m.FileFrames == 0 && m.FileDuration > time.Second {
m.FileFrames = int(math.Round(m.FileFPS * m.FileDuration.Seconds()))
}
}
@@ -671,13 +689,13 @@ func (m *File) SetFrames(n int) {
}
}
-// SetMetaUTC sets the creation date found in the metadata as a unix ms timestamp.
-func (m *File) SetMetaUTC(taken time.Time) {
+// SetMediaUTC sets the media creation date from metadata as unix time in ms.
+func (m *File) SetMediaUTC(taken time.Time) {
if taken.IsZero() {
return
}
- m.MetaUTC = taken.UTC().UnixMilli()
+ m.MediaUTC = taken.UTC().UnixMilli()
}
// AddFaces adds face markers to the file.
@@ -749,7 +767,7 @@ func (m *File) Markers() *Markers {
} else if m.FileUID == "" {
m.markers = &Markers{}
} else if res, err := FindMarkers(m.FileUID); err != nil {
- log.Warnf("file %s: %s while loading markers", sanitize.Log(m.FileUID), err)
+ log.Warnf("file %s: %s while loading markers", clean.Log(m.FileUID), err)
m.markers = &Markers{}
} else {
m.markers = &res
diff --git a/internal/entity/file_fixtures.go b/internal/entity/file_fixtures.go
index 32cb938f1..2bdb0f3f8 100644
--- a/internal/entity/file_fixtures.go
+++ b/internal/entity/file_fixtures.go
@@ -2,6 +2,9 @@ package entity
import (
"time"
+
+ "github.com/photoprism/photoprism/pkg/fs"
+ "github.com/photoprism/photoprism/pkg/media"
)
type FileMap map[string]File
@@ -81,6 +84,7 @@ var FileFixtures = FileMap{
FileSize: 661858,
FileCodec: "jpeg",
FileType: "raw",
+ MediaType: string(media.Raw),
FileMime: "image/DNG",
FilePrimary: false,
FileSidecar: false,
@@ -122,6 +126,7 @@ var FileFixtures = FileMap{
FileSize: 858,
FileCodec: "",
FileType: "xmp",
+ MediaType: string(media.Sidecar),
FileMime: "",
FilePrimary: false,
FileSidecar: true,
@@ -163,6 +168,7 @@ var FileFixtures = FileMap{
FileSize: 961858,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -204,6 +210,7 @@ var FileFixtures = FileMap{
FileSize: 81858,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -245,6 +252,7 @@ var FileFixtures = FileMap{
FileSize: 500,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -286,6 +294,7 @@ var FileFixtures = FileMap{
FileSize: 500,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -327,6 +336,7 @@ var FileFixtures = FileMap{
FileSize: 500,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -368,6 +378,7 @@ var FileFixtures = FileMap{
FileSize: 7799202,
FileCodec: "avc1",
FileType: "mp4",
+ MediaType: string(media.Video),
FileMime: "video/mp4",
FilePrimary: false,
FileSidecar: false,
@@ -409,6 +420,7 @@ var FileFixtures = FileMap{
FileSize: 7799202,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -450,6 +462,7 @@ var FileFixtures = FileMap{
FileSize: 500,
FileCodec: "avc1",
FileType: "mp4",
+ MediaType: string(media.Video),
FileMime: "video/mp4",
FilePrimary: false,
FileSidecar: false,
@@ -491,6 +504,7 @@ var FileFixtures = FileMap{
FileSize: 961851,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -532,6 +546,7 @@ var FileFixtures = FileMap{
FileSize: 921858,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -573,6 +588,7 @@ var FileFixtures = FileMap{
FileSize: 921851,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: false,
FileSidecar: false,
@@ -614,6 +630,7 @@ var FileFixtures = FileMap{
FileSize: 921851,
FileCodec: "avc1",
FileType: "mp4",
+ MediaType: string(media.Video),
FileMime: "image/mp4",
FilePrimary: false,
FileSidecar: false,
@@ -655,6 +672,7 @@ var FileFixtures = FileMap{
FileSize: 921831,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -696,6 +714,7 @@ var FileFixtures = FileMap{
FileSize: 921831,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -737,6 +756,7 @@ var FileFixtures = FileMap{
FileSize: 900,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -778,6 +798,7 @@ var FileFixtures = FileMap{
FileSize: 900,
FileCodec: "",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -819,6 +840,7 @@ var FileFixtures = FileMap{
FileSize: 900,
FileCodec: "avc1",
FileType: "mp4",
+ MediaType: string(media.Video),
FileMime: "video/mp4",
FilePrimary: false,
FileSidecar: false,
@@ -860,6 +882,7 @@ var FileFixtures = FileMap{
FileSize: 900,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -901,6 +924,7 @@ var FileFixtures = FileMap{
FileSize: 900,
FileCodec: "jpeg",
FileType: "raw",
+ MediaType: string(media.Raw),
FileMime: "image/x-canon-cr2",
FilePrimary: false,
FileSidecar: false,
@@ -942,6 +966,7 @@ var FileFixtures = FileMap{
FileSize: 900,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -2538,7 +2563,8 @@ var FileFixtures = FileMap{
FileHash: "pcad9168fa6acc5c5c2965adf6ec465ca42fd3451",
FileSize: 921858,
FileCodec: "jpeg",
- FileType: "jpg",
+ FileType: string(fs.ImageJPEG),
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -2580,6 +2606,7 @@ var FileFixtures = FileMap{
FileSize: 921858,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
@@ -2621,6 +2648,7 @@ var FileFixtures = FileMap{
FileSize: 921858,
FileCodec: "jpeg",
FileType: "jpg",
+ MediaType: string(media.Image),
FileMime: "image/jpg",
FilePrimary: true,
FileSidecar: false,
diff --git a/internal/entity/file_json.go b/internal/entity/file_json.go
index a76fab9e7..c11e6cbd5 100644
--- a/internal/entity/file_json.go
+++ b/internal/entity/file_json.go
@@ -15,14 +15,14 @@ func (m *File) MarshalJSON() ([]byte, error) {
Hash string
Size int64
Primary bool
- MetaUTC int64 `json:",omitempty"`
TimeIndex *string `json:",omitempty"`
MediaID *string `json:",omitempty"`
+ MediaUTC int64 `json:",omitempty"`
InstanceID string `json:",omitempty"`
OriginalName string `json:",omitempty"`
Codec string `json:",omitempty"`
- FileType string `json:"FileType"`
- MediaType string `json:"MediaType"`
+ FileType string `json:",omitempty"`
+ MediaType string `json:",omitempty"`
Mime string `json:",omitempty"`
Sidecar bool `json:",omitempty"`
Missing bool `json:",omitempty"`
@@ -61,7 +61,7 @@ func (m *File) MarshalJSON() ([]byte, error) {
Hash: m.FileHash,
Size: m.FileSize,
Primary: m.FilePrimary,
- MetaUTC: m.MetaUTC,
+ MediaUTC: m.MediaUTC,
TimeIndex: m.TimeIndex,
MediaID: m.MediaID,
InstanceID: m.InstanceID,
diff --git a/internal/entity/file_test.go b/internal/entity/file_test.go
index 67790c278..ca4f00609 100644
--- a/internal/entity/file_test.go
+++ b/internal/entity/file_test.go
@@ -7,8 +7,10 @@ import (
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/face"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs"
+ "github.com/photoprism/photoprism/pkg/projection"
)
func TestFirstFileByHash(t *testing.T) {
@@ -35,7 +37,7 @@ func TestFile_ShareFileName(t *testing.T) {
filename := file.ShareBase(0)
assert.Contains(t, filename, "20190115-000000-Berlin-Morning-Mood")
- assert.Contains(t, filename, fs.JpegExt)
+ assert.Contains(t, filename, fs.ExtJPEG)
})
t.Run("photo without title", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: ""}
@@ -290,11 +292,11 @@ func TestFile_Panorama(t *testing.T) {
assert.False(t, file.Panorama())
})
t.Run("equirectangular", func(t *testing.T) {
- file := &File{Photo: nil, FileType: "jpg", FileSidecar: false, FileWidth: 1500, FileHeight: 1000, FileProjection: ProjEquirectangular}
+ file := &File{Photo: nil, FileType: "jpg", FileSidecar: false, FileWidth: 1500, FileHeight: 1000, FileProjection: projection.Equirectangular.String()}
assert.True(t, file.Panorama())
})
t.Run("transverse-cylindrical", func(t *testing.T) {
- file := &File{Photo: nil, FileType: "jpg", FileSidecar: false, FileWidth: 1500, FileHeight: 1000, FileProjection: ProjTransverseCylindrical}
+ file := &File{Photo: nil, FileType: "jpg", FileSidecar: false, FileWidth: 1500, FileHeight: 1000, FileProjection: projection.TransverseCylindrical.String()}
assert.True(t, file.Panorama())
})
t.Run("sidecar", func(t *testing.T) {
@@ -304,36 +306,42 @@ func TestFile_Panorama(t *testing.T) {
}
func TestFile_SetProjection(t *testing.T) {
- t.Run(ProjDefault, func(t *testing.T) {
+ t.Run("Unknown", func(t *testing.T) {
m := &File{}
- m.SetProjection(ProjDefault)
- assert.Equal(t, ProjDefault, m.FileProjection)
+ m.SetProjection(Unknown)
+ assert.True(t, projection.Unknown.Equal(m.FileProjection))
+ assert.Equal(t, Unknown, m.FileProjection)
+ assert.Equal(t, projection.Unknown.String(), m.FileProjection)
})
- t.Run(ProjCubestrip, func(t *testing.T) {
+ t.Run(projection.Cubestrip.String(), func(t *testing.T) {
m := &File{}
- m.SetProjection(ProjCubestrip)
- assert.Equal(t, ProjCubestrip, m.FileProjection)
+ m.SetProjection(projection.Cubestrip.String())
+ assert.True(t, projection.Cubestrip.Equal(m.FileProjection))
+ assert.Equal(t, projection.Cubestrip.String(), m.FileProjection)
})
- t.Run(ProjCylindrical, func(t *testing.T) {
+ t.Run(projection.Cylindrical.String(), func(t *testing.T) {
m := &File{}
- m.SetProjection(ProjCylindrical)
- assert.Equal(t, ProjCylindrical, m.FileProjection)
+ m.SetProjection(projection.Cylindrical.String())
+ assert.True(t, projection.Cylindrical.Equal(m.FileProjection))
+ assert.Equal(t, projection.Cylindrical.String(), m.FileProjection)
})
- t.Run(ProjTransverseCylindrical, func(t *testing.T) {
+ t.Run(projection.TransverseCylindrical.String(), func(t *testing.T) {
m := &File{}
- m.SetProjection(ProjTransverseCylindrical)
- assert.Equal(t, ProjTransverseCylindrical, m.FileProjection)
+ m.SetProjection(projection.TransverseCylindrical.String())
+ assert.Equal(t, projection.TransverseCylindrical.String(), m.FileProjection)
})
- t.Run(ProjPseudocylindricalCompromise, func(t *testing.T) {
+ t.Run(projection.PseudocylindricalCompromise.String(), func(t *testing.T) {
m := &File{}
- m.SetProjection(ProjPseudocylindricalCompromise)
- assert.Equal(t, ProjPseudocylindricalCompromise, m.FileProjection)
+ m.SetProjection(projection.PseudocylindricalCompromise.String())
+ assert.Equal(t, projection.PseudocylindricalCompromise.String(), m.FileProjection)
+ assert.Equal(t, projection.PseudocylindricalCompromise, projection.Find(m.FileProjection))
})
- t.Run("Sanitize", func(t *testing.T) {
+ t.Run("New", func(t *testing.T) {
m := &File{}
- m.SetProjection(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")
- assert.Equal(t, "hanzi are logograms developed for the writing of chinese! expres", m.FileProjection)
- assert.Equal(t, ClipStringType, len(m.FileProjection))
+ p := projection.New(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")
+ m.SetProjection(p.String())
+ assert.Equal(t, p.String(), m.FileProjection)
+ assert.GreaterOrEqual(t, clean.ClipType, len(m.FileProjection))
})
}
@@ -384,12 +392,12 @@ func TestFile_OriginalBase(t *testing.T) {
filename := file.OriginalBase(0)
assert.Contains(t, filename, "20190115-000000-Berlin-Morning-Mood")
- assert.Contains(t, filename, fs.JpegExt)
+ assert.Contains(t, filename, fs.ExtJPEG)
filename2 := file.OriginalBase(1)
assert.Contains(t, filename2, "20190115-000000-Berlin-Morning-Mood")
assert.Contains(t, filename2, "(1)")
- assert.Contains(t, filename2, fs.JpegExt)
+ assert.Contains(t, filename2, fs.ExtJPEG)
})
t.Run("original name empty", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"}
@@ -398,12 +406,12 @@ func TestFile_OriginalBase(t *testing.T) {
filename := file.OriginalBase(0)
assert.Contains(t, filename, "sonnenaufgang")
- assert.Contains(t, filename, fs.JpegExt)
+ assert.Contains(t, filename, fs.ExtJPEG)
filename2 := file.OriginalBase(1)
assert.Contains(t, filename2, "sonnenaufgang")
assert.Contains(t, filename2, "(1)")
- assert.Contains(t, filename2, fs.JpegExt)
+ assert.Contains(t, filename2, fs.ExtJPEG)
})
t.Run("original name not empty", func(t *testing.T) {
photo := &Photo{TakenAtLocal: time.Date(2019, 01, 15, 0, 0, 0, 0, time.UTC), PhotoTitle: "Berlin / Morning Mood"}
@@ -412,12 +420,12 @@ func TestFile_OriginalBase(t *testing.T) {
filename := file.OriginalBase(0)
assert.Contains(t, filename, "Sonnenaufgang")
- assert.Contains(t, filename, fs.JpegExt)
+ assert.Contains(t, filename, fs.ExtJPEG)
filename2 := file.OriginalBase(1)
assert.Contains(t, filename2, "Sonnenaufgang")
assert.Contains(t, filename2, "(1)")
- assert.Contains(t, filename2, fs.JpegExt)
+ assert.Contains(t, filename2, fs.ExtJPEG)
})
}
@@ -428,12 +436,12 @@ func TestFile_DownloadName(t *testing.T) {
filename := file.DownloadName(DownloadNameFile, 0)
assert.Contains(t, filename, "filename")
- assert.Contains(t, filename, fs.JpegExt)
+ assert.Contains(t, filename, fs.ExtJPEG)
filename2 := file.DownloadName(DownloadNameOriginal, 1)
assert.Contains(t, filename2, "originalName")
assert.Contains(t, filename2, "(1)")
- assert.Contains(t, filename2, fs.JpegExt)
+ assert.Contains(t, filename2, fs.ExtJPEG)
filename3 := file.DownloadName("xxx", 0)
assert.Contains(t, filename3, "20190115-000000-Berlin-Morning-Mood")
diff --git a/internal/entity/folder.go b/internal/entity/folder.go
index e989101b6..e844dcf10 100644
--- a/internal/entity/folder.go
+++ b/internal/entity/folder.go
@@ -8,11 +8,12 @@ import (
"time"
"github.com/jinzhu/gorm"
- "github.com/photoprism/photoprism/internal/form"
- "github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
- "github.com/photoprism/photoprism/pkg/txt"
"github.com/ulule/deepcopier"
+
+ "github.com/photoprism/photoprism/internal/form"
+ "github.com/photoprism/photoprism/pkg/clean"
+ "github.com/photoprism/photoprism/pkg/rnd"
+ "github.com/photoprism/photoprism/pkg/txt"
)
var folderMutex = sync.Mutex{}
@@ -51,11 +52,11 @@ func (Folder) TableName() string {
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Folder) BeforeCreate(scope *gorm.Scope) error {
- if rnd.IsUID(m.FolderUID, 'd') {
+ if rnd.ValidID(m.FolderUID, 'd') {
return nil
}
- return scope.SetColumn("FolderUID", rnd.PPID('d'))
+ return scope.SetColumn("FolderUID", rnd.GenerateUID('d'))
}
// NewFolder creates a new file system directory entity.
@@ -77,7 +78,7 @@ func NewFolder(root, pathName string, modTime time.Time) Folder {
}
result := Folder{
- FolderUID: rnd.PPID('d'),
+ FolderUID: rnd.GenerateUID('d'),
Root: root,
Path: pathName,
FolderType: MediaUnknown,
@@ -185,7 +186,7 @@ func (m *Folder) Create() error {
if err := a.Create(); err != nil {
log.Errorf("folder: %s (add album)", err)
} else {
- log.Infof("folder: added album %s (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Infof("folder: added album %s (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
}
}
diff --git a/internal/entity/label.go b/internal/entity/label.go
index 66a9246c0..13e292f7a 100644
--- a/internal/entity/label.go
+++ b/internal/entity/label.go
@@ -9,8 +9,8 @@ import (
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/event"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -47,11 +47,11 @@ func (Label) TableName() string {
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Label) BeforeCreate(scope *gorm.Scope) error {
- if rnd.IsUID(m.LabelUID, 'l') {
+ if rnd.ValidID(m.LabelUID, 'l') {
return nil
}
- return scope.SetColumn("LabelUID", rnd.PPID('l'))
+ return scope.SetColumn("LabelUID", rnd.GenerateUID('l'))
}
// NewLabel returns a new label.
@@ -164,7 +164,7 @@ func (m *Label) AfterCreate(scope *gorm.Scope) error {
// SetName changes the label name.
func (m *Label) SetName(name string) {
- name = sanitize.Name(name)
+ name = clean.Name(name)
if name == "" {
return
@@ -221,7 +221,7 @@ func (m *Label) UpdateClassify(label classify.Label) error {
}
if err := db.Model(m).Association("LabelCategories").Append(sn).Error; err != nil {
- log.Debugf("index: failed saving label category %s (%s)", sanitize.Log(category), err)
+ log.Debugf("index: failed saving label category %s (%s)", clean.Log(category), err)
}
}
}
diff --git a/internal/entity/lens.go b/internal/entity/lens.go
index 072c61884..bdf2599f6 100644
--- a/internal/entity/lens.go
+++ b/internal/entity/lens.go
@@ -6,7 +6,7 @@ import (
"time"
"github.com/photoprism/photoprism/internal/event"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -125,7 +125,7 @@ func FirstOrCreateLens(m *Lens) *Lens {
lensCache.SetDefault(m.LensSlug, &result)
return &result
} else {
- log.Errorf("lens: %s (create %s)", err.Error(), sanitize.Log(m.String()))
+ log.Errorf("lens: %s (create %s)", err.Error(), clean.Log(m.String()))
}
return &UnknownLens
@@ -133,7 +133,7 @@ func FirstOrCreateLens(m *Lens) *Lens {
// String returns an identifier that can be used in logs.
func (m *Lens) String() string {
- return sanitize.Log(m.LensName)
+ return clean.Log(m.LensName)
}
// Unknown returns true if the lens is not a known make or model.
diff --git a/internal/entity/link.go b/internal/entity/link.go
index 2a5460e19..9ac12d00d 100644
--- a/internal/entity/link.go
+++ b/internal/entity/link.go
@@ -6,8 +6,8 @@ import (
"github.com/jinzhu/gorm"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -31,11 +31,11 @@ type Link struct {
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Link) BeforeCreate(scope *gorm.Scope) error {
- if rnd.IsUID(m.LinkUID, 's') {
+ if rnd.ValidID(m.LinkUID, 's') {
return nil
}
- return scope.SetColumn("LinkUID", rnd.PPID('s'))
+ return scope.SetColumn("LinkUID", rnd.GenerateUID('s'))
}
// NewLink creates a sharing link.
@@ -43,9 +43,9 @@ func NewLink(shareUID string, canComment, canEdit bool) Link {
now := TimeStamp()
result := Link{
- LinkUID: rnd.PPID('s'),
+ LinkUID: rnd.GenerateUID('s'),
ShareUID: shareUID,
- LinkToken: rnd.Token(10),
+ LinkToken: rnd.GenerateToken(10),
CanComment: canComment,
CanEdit: canEdit,
CreatedAt: now,
@@ -112,7 +112,7 @@ func (m *Link) InvalidPassword(password string) bool {
// Save inserts a new row to the database or updates a row if the primary key already exists.
func (m *Link) Save() error {
- if !rnd.IsPPID(m.ShareUID, 0) {
+ if !rnd.EntityUID(m.ShareUID, 0) {
return fmt.Errorf("link: invalid share uid (%s)", m.ShareUID)
}
@@ -166,7 +166,7 @@ func FindLinks(token, share string) (result Links) {
}
if share != "" {
- if rnd.IsPPID(share, 'a') {
+ if rnd.EntityUID(share, 'a') {
q = q.Where("share_uid = ?", share)
} else {
q = q.Where("share_slug = ?", share)
@@ -193,5 +193,5 @@ func FindValidLinks(token, share string) (result Links) {
// String returns an human readable identifier for logging.
func (m *Link) String() string {
- return sanitize.Log(m.LinkUID)
+ return clean.Log(m.LinkUID)
}
diff --git a/internal/entity/link_test.go b/internal/entity/link_test.go
index 7c37267eb..88d95bc3f 100644
--- a/internal/entity/link_test.go
+++ b/internal/entity/link_test.go
@@ -46,7 +46,7 @@ func TestLink_Expired(t *testing.T) {
}
func TestLink_Redeem(t *testing.T) {
- link := NewLink(rnd.PPID('a'), false, false)
+ link := NewLink(rnd.GenerateUID('a'), false, false)
assert.Equal(t, uint(0), link.LinkViews)
diff --git a/internal/entity/marker.go b/internal/entity/marker.go
index 57a82a1a0..c19adba95 100644
--- a/internal/entity/marker.go
+++ b/internal/entity/marker.go
@@ -14,8 +14,8 @@ import (
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/form"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
const (
@@ -62,11 +62,11 @@ func (Marker) TableName() string {
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
- if rnd.IsUID(m.MarkerUID, 'm') {
+ if rnd.ValidID(m.MarkerUID, 'm') {
return nil
}
- return scope.SetColumn("MarkerUID", rnd.PPID('m'))
+ return scope.SetColumn("MarkerUID", rnd.GenerateUID('m'))
}
// NewMarker creates a new entity.
@@ -157,7 +157,7 @@ func (m *Marker) SetName(name, src string) (changed bool, err error) {
return false, nil
}
- name = sanitize.Name(name)
+ name = clean.Name(name)
if name == "" {
return false, nil
@@ -232,7 +232,7 @@ func (m *Marker) SetFace(f *Face, dist float64) (updated bool, err error) {
} else if reported, err := f.ResolveCollision(m.Embeddings()); err != nil {
return false, err
} else if reported {
- log.Warnf("faces: marker %s face %s has ambiguous subjects %s <> %s, subject source %s", sanitize.Log(m.MarkerUID), sanitize.Log(f.ID), sanitize.Log(m.SubjUID), sanitize.Log(f.SubjUID), SrcString(m.SubjSrc))
+ log.Warnf("faces: marker %s face %s has ambiguous subjects %s <> %s, subject source %s", clean.Log(m.MarkerUID), clean.Log(f.ID), clean.Log(m.SubjUID), clean.Log(f.SubjUID), SrcString(m.SubjSrc))
return false, nil
} else {
return false, nil
@@ -428,10 +428,10 @@ func (m *Marker) Subject() (subj *Subject) {
// Create subject?
if m.SubjSrc != SrcAuto && m.MarkerName != "" && m.SubjUID == "" {
if subj = NewSubject(m.MarkerName, SubjPerson, m.SubjSrc); subj == nil {
- log.Errorf("faces: marker %s has invalid subject %s", sanitize.Log(m.MarkerUID), sanitize.Log(m.MarkerName))
+ log.Errorf("faces: marker %s has invalid subject %s", clean.Log(m.MarkerUID), clean.Log(m.MarkerName))
return nil
} else if subj = FirstOrCreateSubject(subj); subj == nil {
- log.Debugf("faces: marker %s has invalid subject %s", sanitize.Log(m.MarkerUID), sanitize.Log(m.MarkerName))
+ log.Debugf("faces: marker %s has invalid subject %s", clean.Log(m.MarkerUID), clean.Log(m.MarkerName))
return nil
} else {
m.subject = subj
@@ -457,9 +457,9 @@ func (m *Marker) ClearSubject(src string) error {
// Find and (soft) delete unused subjects.
start := time.Now()
if count, err := DeleteOrphanPeople(); err != nil {
- log.Errorf("faces: %s while clearing subject of marker %s [%s]", err, sanitize.Log(m.MarkerUID), time.Since(start))
+ log.Errorf("faces: %s while clearing subject of marker %s [%s]", err, clean.Log(m.MarkerUID), time.Since(start))
} else if count > 0 {
- log.Debugf("faces: %s marked as missing while clearing subject of marker %s [%s]", english.Plural(count, "person", "people"), sanitize.Log(m.MarkerUID), time.Since(start))
+ log.Debugf("faces: %s marked as missing while clearing subject of marker %s [%s]", english.Plural(count, "person", "people"), clean.Log(m.MarkerUID), time.Since(start))
}
}()
@@ -472,7 +472,7 @@ func (m *Marker) ClearSubject(src string) error {
} else if resolved, err := m.face.ResolveCollision(m.Embeddings()); err != nil {
return err
} else if resolved {
- log.Debugf("faces: marker %s resolved ambiguous subjects for face %s", sanitize.Log(m.MarkerUID), sanitize.Log(m.face.ID))
+ log.Debugf("faces: marker %s resolved ambiguous subjects for face %s", clean.Log(m.MarkerUID), clean.Log(m.face.ID))
}
// Clear references.
@@ -498,23 +498,23 @@ func (m *Marker) Face() (f *Face) {
// Add face if size
if m.SubjSrc != SrcAuto && m.FaceID == "" {
if m.Size < face.ClusterSizeThreshold || m.Score < face.ClusterScoreThreshold {
- log.Debugf("faces: marker %s skipped adding face due to low-quality (size %d, score %d)", sanitize.Log(m.MarkerUID), m.Size, m.Score)
+ log.Debugf("faces: marker %s skipped adding face due to low-quality (size %d, score %d)", clean.Log(m.MarkerUID), m.Size, m.Score)
return nil
}
if emb := m.Embeddings(); emb.Empty() {
- log.Warnf("faces: marker %s has no face embeddings", sanitize.Log(m.MarkerUID))
+ log.Warnf("faces: marker %s has no face embeddings", clean.Log(m.MarkerUID))
return nil
} else if f = NewFace(m.SubjUID, m.SubjSrc, emb); f == nil {
- log.Warnf("faces: failed assigning face to marker %s", sanitize.Log(m.MarkerUID))
+ log.Warnf("faces: failed assigning face to marker %s", clean.Log(m.MarkerUID))
return nil
} else if f.SkipMatching() {
- log.Infof("faces: skipped matching marker %s, embedding %s not distinct enough", sanitize.Log(m.MarkerUID), f.ID)
+ log.Infof("faces: skipped matching marker %s, embedding %s not distinct enough", clean.Log(m.MarkerUID), f.ID)
} else if f = FirstOrCreateFace(f); f == nil {
- log.Warnf("faces: failed matching marker %s with subject %s", sanitize.Log(m.MarkerUID), SubjNames.Log(m.SubjUID))
+ log.Warnf("faces: failed matching marker %s with subject %s", clean.Log(m.MarkerUID), SubjNames.Log(m.SubjUID))
return nil
} else if err := f.MatchMarkers(Faceless); err != nil {
- log.Errorf("faces: failed matching marker %s with subject %s (%s)", sanitize.Log(m.MarkerUID), SubjNames.Log(m.SubjUID), err)
+ log.Errorf("faces: failed matching marker %s with subject %s (%s)", clean.Log(m.MarkerUID), SubjNames.Log(m.SubjUID), err)
}
m.face = f
@@ -709,7 +709,7 @@ func CreateMarkerIfNotExists(m *Marker) (*Marker, error) {
} else if err := m.Create(); err != nil {
return m, err
} else {
- log.Debugf("markers: added %s %s for file %s", TypeString(m.MarkerType), sanitize.Log(m.MarkerUID), sanitize.Log(m.FileUID))
+ log.Debugf("markers: added %s %s for file %s", TypeString(m.MarkerType), clean.Log(m.MarkerUID), clean.Log(m.FileUID))
}
return m, nil
diff --git a/internal/entity/photo.go b/internal/entity/photo.go
index af72a5ab8..414fb560c 100644
--- a/internal/entity/photo.go
+++ b/internal/entity/photo.go
@@ -16,8 +16,8 @@ import (
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -211,15 +211,15 @@ func SavePhotoForm(model Photo, form form.Photo) error {
func (m *Photo) String() string {
if m.PhotoUID == "" {
if m.PhotoName != "" {
- return sanitize.Log(m.PhotoName)
+ return clean.Log(m.PhotoName)
} else if m.OriginalName != "" {
- return sanitize.Log(m.OriginalName)
+ return clean.Log(m.OriginalName)
}
return "(unknown)"
}
- return "uid " + sanitize.Log(m.PhotoUID)
+ return "uid " + clean.Log(m.PhotoUID)
}
// FirstOrCreate fetches an existing row from the database or inserts a new one.
@@ -286,7 +286,7 @@ func (m *Photo) Find() error {
Preload("Cell").
Preload("Cell.Place")
- if rnd.IsPPID(m.PhotoUID, 'p') {
+ if rnd.EntityUID(m.PhotoUID, 'p') {
if err := q.First(m, "photo_uid = ?", m.PhotoUID).Error; err != nil {
return err
}
@@ -365,11 +365,11 @@ func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
}
}
- if rnd.IsUID(m.PhotoUID, 'p') {
+ if rnd.ValidID(m.PhotoUID, 'p') {
return nil
}
- return scope.SetColumn("PhotoUID", rnd.PPID('p'))
+ return scope.SetColumn("PhotoUID", rnd.GenerateUID('p'))
}
// BeforeSave ensures the existence of TakenAt properties before indexing or updating a photo
@@ -562,17 +562,17 @@ func (m *Photo) AddLabels(labels classify.Labels) {
labelEntity := FirstOrCreateLabel(NewLabel(classifyLabel.Title(), classifyLabel.Priority))
if labelEntity == nil {
- log.Errorf("index: label %s should not be nil - possible bug (%s)", sanitize.Log(classifyLabel.Title()), m)
+ log.Errorf("index: label %s should not be nil - possible bug (%s)", clean.Log(classifyLabel.Title()), m)
continue
}
if labelEntity.Deleted() {
- log.Debugf("index: skipping deleted label %s (%s)", sanitize.Log(classifyLabel.Title()), m)
+ log.Debugf("index: skipping deleted label %s (%s)", clean.Log(classifyLabel.Title()), m)
continue
}
if err := labelEntity.UpdateClassify(classifyLabel); err != nil {
- log.Errorf("index: failed updating label %s (%s)", sanitize.Log(classifyLabel.Title()), err)
+ log.Errorf("index: failed updating label %s (%s)", clean.Log(classifyLabel.Title()), err)
}
photoLabel := FirstOrCreatePhotoLabel(NewPhotoLabel(m.ID, labelEntity.ID, classifyLabel.Uncertainty, classifyLabel.Source))
@@ -722,7 +722,7 @@ func (m *Photo) Restore() error {
// Delete deletes the photo from the index.
func (m *Photo) Delete(permanently bool) (files Files, err error) {
if m.ID < 1 || m.PhotoUID == "" {
- return files, fmt.Errorf("invalid photo id %d / uid %s", m.ID, sanitize.Log(m.PhotoUID))
+ return files, fmt.Errorf("invalid photo id %d / uid %s", m.ID, clean.Log(m.PhotoUID))
}
if permanently {
@@ -743,7 +743,7 @@ func (m *Photo) Delete(permanently bool) (files Files, err error) {
// DeletePermanently permanently removes a photo from the index.
func (m *Photo) DeletePermanently() (files Files, err error) {
if m.ID < 1 || m.PhotoUID == "" {
- return files, fmt.Errorf("invalid photo id %d / uid %s", m.ID, sanitize.Log(m.PhotoUID))
+ return files, fmt.Errorf("invalid photo id %d / uid %s", m.ID, clean.Log(m.PhotoUID))
}
files = m.AllFiles()
diff --git a/internal/entity/photo_estimate.go b/internal/entity/photo_estimate.go
index 8e1c7bbd7..5b522e6e9 100644
--- a/internal/entity/photo_estimate.go
+++ b/internal/entity/photo_estimate.go
@@ -5,9 +5,9 @@ import (
"github.com/jinzhu/gorm"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/geo"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -53,7 +53,7 @@ func (m *Photo) EstimateCountry() {
m.PhotoCountry = countryCode
m.PlaceSrc = SrcEstimate
m.EstimatedAt = TimePointer()
- log.Debugf("photo: estimated country for %s is %s", m, sanitize.Log(m.CountryName()))
+ log.Debugf("photo: estimated country for %s is %s", m, clean.Log(m.CountryName()))
}
}
@@ -110,7 +110,7 @@ func (m *Photo) EstimateLocation(force bool) {
Order(gorm.Expr("ABS(JulianDay(taken_at) - JulianDay(?))", m.TakenAt)).Limit(2).
Preload("Place").Find(&mostRecent).Error
default:
- log.Warnf("photo: unsupported sql dialect %s", sanitize.Log(DbDialect()))
+ log.Warnf("photo: unsupported sql dialect %s", clean.Log(DbDialect()))
return
}
@@ -147,7 +147,7 @@ func (m *Photo) EstimateLocation(force bool) {
}
}
} else if recentPhoto.HasCountry() {
- log.Debugf("photo: estimated country for %s is %s", m, sanitize.Log(m.CountryName()))
+ log.Debugf("photo: estimated country for %s is %s", m, clean.Log(m.CountryName()))
m.RemoveLocation(SrcEstimate, false)
m.RemoveLocationLabels()
m.PhotoCountry = recentPhoto.PhotoCountry
diff --git a/internal/entity/photo_location.go b/internal/entity/photo_location.go
index 1b787c058..7cd304de4 100644
--- a/internal/entity/photo_location.go
+++ b/internal/entity/photo_location.go
@@ -9,8 +9,8 @@ import (
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/maps"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/geo"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
"gopkg.in/photoprism/go-tz.v2/tz"
)
@@ -78,7 +78,7 @@ func (m *Photo) SetPosition(pos geo.Position, source string, force bool) {
if m.Place == nil {
log.Warnf("photo: failed updating position of %s", m)
} else {
- log.Debugf("photo: approximate place of %s is %s (id %s)", m, sanitize.Log(m.Place.Label()), m.PlaceID)
+ log.Debugf("photo: approximate place of %s is %s (id %s)", m, clean.Log(m.Place.Label()), m.PlaceID)
}
}
}
@@ -107,7 +107,7 @@ func (m *Photo) AdoptPlace(other Photo, source string, force bool) {
m.UpdateTimeZone(other.TimeZone)
- log.Debugf("photo: %s now located at %s (id %s)", m.String(), sanitize.Log(m.Place.Label()), m.PlaceID)
+ log.Debugf("photo: %s now located at %s (id %s)", m.String(), clean.Log(m.Place.Label()), m.PlaceID)
}
// RemoveLocation removes the current location.
diff --git a/internal/entity/photo_merge.go b/internal/entity/photo_merge.go
index 6028667b5..2a0533e77 100644
--- a/internal/entity/photo_merge.go
+++ b/internal/entity/photo_merge.go
@@ -38,7 +38,7 @@ func (m *Photo) Identical(includeMeta, includeUuid bool) (identical Photos, err
}
includeMeta = includeMeta && m.TrustedLocation() && m.TrustedTime()
- includeUuid = includeUuid && rnd.IsUUID(m.UUID)
+ includeUuid = includeUuid && rnd.ValidUUID(m.UUID)
switch {
case includeMeta && includeUuid:
diff --git a/internal/entity/photo_title.go b/internal/entity/photo_title.go
index f433f5be6..fcd9d241e 100644
--- a/internal/entity/photo_title.go
+++ b/internal/entity/photo_title.go
@@ -9,8 +9,8 @@ import (
"github.com/dustin/go-humanize/english"
"github.com/photoprism/photoprism/internal/classify"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -69,7 +69,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
// TODO: User defined title format
if names != "" {
- log.Debugf("photo: %s title based on %s (%s)", m.String(), english.Plural(len(people), "person", "people"), sanitize.Log(names))
+ log.Debugf("photo: %s title based on %s (%s)", m.String(), english.Plural(len(people), "person", "people"), clean.Log(names))
if l := len([]rune(names)); l > 35 {
m.SetTitle(names, SrcAuto)
@@ -83,7 +83,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
m.SetTitle(fmt.Sprintf("%s / %s / %s", names, loc.City(), m.TakenAt.Format("2006")), SrcAuto)
}
} else if title := labels.Title(loc.Name()); title != "" {
- log.Debugf("photo: %s title based on label %s", m.String(), sanitize.Log(title))
+ log.Debugf("photo: %s title based on label %s", m.String(), clean.Log(title))
if loc.NoCity() || loc.LongCity() || loc.CityContains(title) {
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), loc.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
} else {
@@ -108,7 +108,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
knownLocation = true
if names != "" {
- log.Debugf("photo: %s title based on %s (%s)", m.String(), english.Plural(len(people), "person", "people"), sanitize.Log(names))
+ log.Debugf("photo: %s title based on %s (%s)", m.String(), english.Plural(len(people), "person", "people"), clean.Log(names))
if l := len([]rune(names)); l > 35 {
m.SetTitle(names, SrcAuto)
@@ -122,7 +122,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
m.SetTitle(fmt.Sprintf("%s / %s / %s", names, m.Place.City(), m.TakenAt.Format("2006")), SrcAuto)
}
} else if title := labels.Title(fileTitle); title != "" {
- log.Debugf("photo: %s title based on label %s", m.String(), sanitize.Log(title))
+ log.Debugf("photo: %s title based on label %s", m.String(), clean.Log(title))
if m.Place.NoCity() || m.Place.LongCity() || m.Place.CityContains(title) {
m.SetTitle(fmt.Sprintf("%s / %s / %s", txt.Title(title), m.Place.CountryName(), m.TakenAt.Format("2006")), SrcAuto)
} else {
@@ -164,7 +164,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
}
if m.PhotoTitle != oldTitle {
- log.Debugf("photo: %s has new title %s [%s]", m.String(), sanitize.Log(m.PhotoTitle), time.Since(start))
+ log.Debugf("photo: %s has new title %s [%s]", m.String(), clean.Log(m.PhotoTitle), time.Since(start))
}
return nil
diff --git a/internal/entity/photo_yaml.go b/internal/entity/photo_yaml.go
index 7f2d2b61c..aed175183 100644
--- a/internal/entity/photo_yaml.go
+++ b/internal/entity/photo_yaml.go
@@ -66,5 +66,5 @@ func (m *Photo) LoadFromYaml(fileName string) error {
// YamlFileName returns the YAML file name.
func (m *Photo) YamlFileName(originalsPath, sidecarPath string) string {
- return fs.FileName(filepath.Join(originalsPath, m.PhotoPath, m.PhotoName), sidecarPath, originalsPath, fs.YamlExt)
+ return fs.FileName(filepath.Join(originalsPath, m.PhotoPath, m.PhotoName), sidecarPath, originalsPath, fs.ExtYAML)
}
diff --git a/internal/entity/place.go b/internal/entity/place.go
index 8f916105d..b0e38457f 100644
--- a/internal/entity/place.go
+++ b/internal/entity/place.go
@@ -6,7 +6,7 @@ import (
"time"
"github.com/photoprism/photoprism/internal/maps"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
var placeMutex = sync.Mutex{}
@@ -54,7 +54,7 @@ func FindPlace(id string) *Place {
place := Place{}
if err := Db().Where("id = ?", id).First(&place).Error; err != nil {
- log.Debugf("place: %s not found", sanitize.Log(id))
+ log.Debugf("place: %s not found", clean.Log(id))
return nil
} else {
return &place
diff --git a/internal/entity/string_map.go b/internal/entity/string_map.go
index 7b3078572..4990c5fca 100644
--- a/internal/entity/string_map.go
+++ b/internal/entity/string_map.go
@@ -4,7 +4,7 @@ import (
"strings"
"sync"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Strings is a simple string map that should not be accessed by multiple goroutines.
@@ -63,9 +63,9 @@ func (s *StringMap) Log(key string) (val string) {
}
if val = s.Get(key); val != "" {
- return sanitize.Log(val)
+ return clean.Log(val)
} else {
- return sanitize.Log(key)
+ return clean.Log(key)
}
}
diff --git a/internal/entity/subject.go b/internal/entity/subject.go
index 5cc07dd7c..37213f3fa 100644
--- a/internal/entity/subject.go
+++ b/internal/entity/subject.go
@@ -12,8 +12,8 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -50,11 +50,11 @@ func (Subject) TableName() string {
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *Subject) BeforeCreate(scope *gorm.Scope) error {
- if rnd.IsUID(m.SubjUID, 'j') {
+ if rnd.ValidID(m.SubjUID, 'j') {
return nil
}
- return scope.SetColumn("SubjUID", rnd.PPID('j'))
+ return scope.SetColumn("SubjUID", rnd.GenerateUID('j'))
}
// AfterSave is a hook that updates the name cache after saving.
@@ -131,7 +131,7 @@ func (m *Subject) Delete() error {
return err
}
- log.Infof("subject: marked %s %s as missing", TypeString(m.SubjType), sanitize.Log(m.SubjName))
+ log.Infof("subject: marked %s %s as missing", TypeString(m.SubjType), clean.Log(m.SubjName))
return Db().Delete(m).Error
}
@@ -155,7 +155,7 @@ func (m *Subject) Restore() error {
if m.Deleted() {
m.DeletedAt = nil
- log.Infof("subject: restoring %s %s", TypeString(m.SubjType), sanitize.Log(m.SubjName))
+ log.Infof("subject: restoring %s %s", TypeString(m.SubjType), clean.Log(m.SubjName))
event.EntitiesCreated("subjects", []*Subject{m})
@@ -193,7 +193,7 @@ func FirstOrCreateSubject(m *Subject) *Subject {
if found := FindSubjectByName(m.SubjName); found != nil {
return found
} else if err := m.Create(); err == nil {
- log.Infof("subject: added %s %s", TypeString(m.SubjType), sanitize.Log(m.SubjName))
+ log.Infof("subject: added %s %s", TypeString(m.SubjType), clean.Log(m.SubjName))
event.EntitiesCreated("subjects", []*Subject{m})
@@ -208,7 +208,7 @@ func FirstOrCreateSubject(m *Subject) *Subject {
} else if found = FindSubjectByName(m.SubjName); found != nil {
return found
} else {
- log.Errorf("subject: failed adding %s (%s)", sanitize.Log(m.SubjName), err)
+ log.Errorf("subject: failed adding %s (%s)", clean.Log(m.SubjName), err)
}
return nil
@@ -231,7 +231,7 @@ func FindSubject(uid string) *Subject {
// FindSubjectByName find an existing subject by name.
func FindSubjectByName(name string) *Subject {
- name = sanitize.Name(name)
+ name = clean.Name(name)
if name == "" {
return nil
@@ -243,12 +243,12 @@ func FindSubjectByName(name string) *Subject {
switch uid {
case "":
if err := UnscopedDb().Where("subj_name LIKE ?", name).First(&result).Error; err != nil {
- log.Debugf("subject: %s not found by name", sanitize.Log(name))
+ log.Debugf("subject: %s not found by name", clean.Log(name))
return nil
}
default:
if found := FindSubject(uid); found == nil {
- log.Debugf("subject: %s not found by uid", sanitize.Log(name))
+ log.Debugf("subject: %s not found by uid", clean.Log(name))
return nil
} else {
result = *found
@@ -259,10 +259,10 @@ func FindSubjectByName(name string) *Subject {
if !result.Deleted() {
return &result
} else if err := result.Restore(); err == nil {
- log.Debugf("subject: restored %s", sanitize.Log(result.SubjName))
+ log.Debugf("subject: restored %s", clean.Log(result.SubjName))
return &result
} else {
- log.Errorf("subject: failed restoring %s (%s)", sanitize.Log(result.SubjName), err)
+ log.Errorf("subject: failed restoring %s (%s)", clean.Log(result.SubjName), err)
}
return nil
@@ -280,7 +280,7 @@ func (m *Subject) Person() *Person {
// SetName changes the subject's name.
func (m *Subject) SetName(name string) error {
- name = sanitize.Name(name)
+ name = clean.Name(name)
if name == m.SubjName {
// Nothing to do.
@@ -307,7 +307,7 @@ func (m *Subject) SaveForm(f form.Subject) (changed bool, err error) {
}
// Change name?
- if name := sanitize.Name(f.SubjName); name != "" && name != m.SubjName {
+ if name := clean.Name(f.SubjName); name != "" && name != m.SubjName {
existing, err := m.UpdateName(name)
if existing.SubjUID != m.SubjUID || err != nil {
@@ -375,7 +375,7 @@ func (m *Subject) UpdateName(name string) (*Subject, error) {
if err := m.SetName(name); err != nil {
return m, err
} else if err := m.Updates(Values{"SubjName": m.SubjName, "SubjSlug": m.SubjSlug}); err == nil {
- log.Infof("subject: renamed %s %s", TypeString(m.SubjType), sanitize.Log(m.SubjName))
+ log.Infof("subject: renamed %s %s", TypeString(m.SubjType), clean.Log(m.SubjName))
event.EntitiesUpdated("subjects", []*Subject{m})
diff --git a/internal/entity/user.go b/internal/entity/user.go
index 4be76732e..333766346 100644
--- a/internal/entity/user.go
+++ b/internal/entity/user.go
@@ -11,8 +11,8 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/form"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
const UsernameLen = 3
@@ -146,10 +146,10 @@ func (m *User) Save() error {
// BeforeCreate creates a random UID if needed before inserting a new row to the database.
func (m *User) BeforeCreate(tx *gorm.DB) error {
- if rnd.IsUID(m.UserUID, 'u') {
+ if rnd.ValidID(m.UserUID, 'u') {
return nil
}
- m.UserUID = rnd.PPID('u')
+ m.UserUID = rnd.GenerateUID('u')
return nil
}
@@ -169,7 +169,7 @@ func FirstOrCreateUser(m *User) *User {
// FindUserByName returns an existing user or nil if not found.
func FindUserByName(userName string) *User {
- userName = sanitize.Username(userName)
+ userName = clean.Username(userName)
if userName == "" {
return nil
@@ -180,7 +180,7 @@ func FindUserByName(userName string) *User {
if err := Db().Preload("Address").Where("user_name = ?", userName).First(&result).Error; err == nil {
return &result
} else {
- log.Debugf("user %s not found", sanitize.Log(userName))
+ log.Debugf("user %s not found", clean.Log(userName))
return nil
}
}
@@ -196,7 +196,7 @@ func FindUserByUID(uid string) *User {
if err := Db().Preload("Address").Where("user_uid = ?", uid).First(&result).Error; err == nil {
return &result
} else {
- log.Debugf("user %s not found", sanitize.Log(uid))
+ log.Debugf("user %s not found", clean.Log(uid))
return nil
}
}
@@ -218,24 +218,24 @@ func (m *User) Deleted() bool {
// String returns an identifier that can be used in logs.
func (m *User) String() string {
if n := m.Username(); n != "" {
- return sanitize.Log(n)
+ return clean.Log(n)
}
if m.FullName != "" {
- return sanitize.Log(m.FullName)
+ return clean.Log(m.FullName)
}
- return sanitize.Log(m.UserUID)
+ return clean.Log(m.UserUID)
}
// Username returns the normalized username.
func (m *User) Username() string {
- return sanitize.Username(m.UserName)
+ return clean.Username(m.UserName)
}
// Registered tests if the user is registered e.g. has a username.
func (m *User) Registered() bool {
- return m.Username() != "" && rnd.IsPPID(m.UserUID, 'u')
+ return m.Username() != "" && rnd.EntityUID(m.UserUID, 'u')
}
// Admin returns true if the user is an admin with user name.
@@ -245,7 +245,7 @@ func (m *User) Admin() bool {
// Anonymous returns true if the user is unknown.
func (m *User) Anonymous() bool {
- return !rnd.IsPPID(m.UserUID, 'u') || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID
+ return !rnd.EntityUID(m.UserUID, 'u') || m.ID == UnknownUser.ID || m.UserUID == UnknownUser.UserUID
}
// Guest returns true if the user is a guest.
@@ -418,7 +418,7 @@ func CreateWithPassword(uc form.UserCreate) error {
if err := tx.Create(&pw).Error; err != nil {
return err
}
- log.Infof("created user %s with uid %s", sanitize.Log(u.Username()), sanitize.Log(u.UserUID))
+ log.Infof("created user %s with uid %s", clean.Log(u.Username()), clean.Log(u.UserUID))
return nil
})
}
diff --git a/internal/face/detector.go b/internal/face/detector.go
index 247801b8c..7139af2c2 100644
--- a/internal/face/detector.go
+++ b/internal/face/detector.go
@@ -11,8 +11,8 @@ import (
"sort"
pigo "github.com/esimov/pigo/core"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
//go:embed cascade/facefinder
@@ -90,7 +90,7 @@ func Detect(fileName string, findLandmarks bool, minSize int) (faces Faces, err
}
if !fs.FileExists(fileName) {
- return faces, fmt.Errorf("faces: file '%s' not found", sanitize.Log(filepath.Base(fileName)))
+ return faces, fmt.Errorf("faces: file '%s' not found", clean.Log(filepath.Base(fileName)))
}
det, params, err := d.Detect(fileName)
diff --git a/internal/face/net.go b/internal/face/net.go
index 68e2a3cd9..ddfba154c 100644
--- a/internal/face/net.go
+++ b/internal/face/net.go
@@ -11,7 +11,7 @@ import (
tf "github.com/tensorflow/tensorflow/tensorflow/go"
"github.com/photoprism/photoprism/internal/crop"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Net is a wrapper for the TensorFlow Facenet model.
@@ -82,7 +82,7 @@ func (t *Net) loadModel() error {
modelPath := path.Join(t.modelPath)
- log.Infof("faces: loading %s", sanitize.Log(filepath.Base(modelPath)))
+ log.Infof("faces: loading %s", clean.Log(filepath.Base(modelPath)))
// Load model
model, err := tf.LoadSavedModel(modelPath, t.modelTags, nil)
diff --git a/internal/ffmpeg/convert.go b/internal/ffmpeg/convert.go
index 99a568749..35355ff16 100644
--- a/internal/ffmpeg/convert.go
+++ b/internal/ffmpeg/convert.go
@@ -19,7 +19,7 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc
useMutex = true
// Animated GIF?
- if fs.FileFormat(fileName) == fs.FormatGif {
+ if fs.FileType(fileName) == fs.ImageGIF {
result = exec.Command(
ffmpegBin,
"-i", fileName,
diff --git a/internal/ffmpeg/encoders.go b/internal/ffmpeg/encoders.go
index 9b8e7eef0..d41e69dc3 100644
--- a/internal/ffmpeg/encoders.go
+++ b/internal/ffmpeg/encoders.go
@@ -1,6 +1,6 @@
package ffmpeg
-import "github.com/photoprism/photoprism/pkg/sanitize"
+import "github.com/photoprism/photoprism/pkg/clean"
// AvcEncoder represents a supported FFmpeg AVC encoder name.
type AvcEncoder string
@@ -56,7 +56,7 @@ func FindEncoder(s string) AvcEncoder {
if encoder, ok := AvcEncoders[s]; ok {
return encoder
} else {
- log.Warnf("ffmpeg: unsupported encoder %s", sanitize.Log(s))
+ log.Warnf("ffmpeg: unsupported encoder %s", clean.Log(s))
}
return SoftwareEncoder
diff --git a/internal/form/report.go b/internal/form/form_report.go
similarity index 88%
rename from internal/form/report.go
rename to internal/form/form_report.go
index fb8439fa5..f9640f0e3 100644
--- a/internal/form/report.go
+++ b/internal/form/form_report.go
@@ -5,7 +5,7 @@ import (
"reflect"
"time"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Report returns form fields as table rows for reports.
@@ -26,9 +26,12 @@ func Report(f interface{}) (rows [][]string, cols []string) {
// Iterate through all form fields.
for i := 0; i < v.NumField(); i++ {
+ // Skip unexported fields.
if !v.Type().Field(i).IsExported() {
continue
}
+
+ // Get info from struct field tags.
fieldValue := v.Field(i)
fieldName := v.Type().Field(i).Tag.Get("form")
fieldInfo := v.Type().Field(i).Tag.Get("serialize")
@@ -68,10 +71,10 @@ func Report(f interface{}) (rows [][]string, cols []string) {
case bool:
typeName = "switch"
if example == "" {
- example = fmt.Sprintf("%s:yes %s:no", fieldName, fieldName)
+ example = fmt.Sprintf("%s:yes", fieldName)
}
default:
- log.Warnf("failed reporting on %T %s", t, sanitize.Token(fieldName))
+ log.Warnf("failed reporting on %T %s", t, clean.Token(fieldName))
continue
}
diff --git a/internal/form/serialize.go b/internal/form/serialize.go
index 886fe7c92..7ea107c86 100644
--- a/internal/form/serialize.go
+++ b/internal/form/serialize.go
@@ -8,7 +8,7 @@ import (
"time"
"unicode"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/araddon/dateparse"
"github.com/photoprism/photoprism/pkg/txt"
@@ -70,7 +70,7 @@ func Serialize(f interface{}, all bool) string {
q = append(q, fmt.Sprintf("%s:%t", fieldName, fieldValue.Bool()))
}
default:
- log.Warnf("form: failed serializing %T %s", t, sanitize.Token(fieldName))
+ log.Warnf("form: failed serializing %T %s", t, clean.Token(fieldName))
}
}
}
@@ -93,7 +93,7 @@ func Unserialize(f SearchForm, q string) (result error) {
for _, char := range q {
if unicode.IsSpace(char) && !escaped {
if isKeyValue {
- fieldName := strings.Title(string(key))
+ fieldName := txt.UpperFirst(string(key))
field := formValues.FieldByNameFunc(func(name string) bool {
return strings.EqualFold(name, fieldName)
})
@@ -126,7 +126,7 @@ func Unserialize(f SearchForm, q string) (result error) {
field.SetUint(uint64(intValue))
}
case string:
- field.SetString(sanitize.SearchString(stringValue))
+ field.SetString(clean.SearchString(stringValue))
case bool:
field.SetBool(txt.Bool(stringValue))
default:
@@ -155,7 +155,7 @@ func Unserialize(f SearchForm, q string) (result error) {
}
if len(queryStrings) > 0 {
- f.SetQuery(sanitize.SearchQuery(strings.Join(queryStrings, " ")))
+ f.SetQuery(clean.SearchQuery(strings.Join(queryStrings, " ")))
}
if result != nil {
diff --git a/internal/form/user.go b/internal/form/user.go
index 91e9610cb..b7a69fe33 100644
--- a/internal/form/user.go
+++ b/internal/form/user.go
@@ -1,6 +1,6 @@
package form
-import "github.com/photoprism/photoprism/pkg/sanitize"
+import "github.com/photoprism/photoprism/pkg/clean"
// UserCreate represents a User with a new password.
type UserCreate struct {
@@ -12,5 +12,5 @@ type UserCreate struct {
// Username returns the normalized username in lowercase and without whitespace padding.
func (f UserCreate) Username() string {
- return sanitize.Username(f.UserName)
+ return clean.Username(f.UserName)
}
diff --git a/internal/hub/config.go b/internal/hub/config.go
index 5e6e097f9..60d015831 100644
--- a/internal/hub/config.go
+++ b/internal/hub/config.go
@@ -19,8 +19,8 @@ import (
"gopkg.in/yaml.v2"
"github.com/photoprism/photoprism/internal/hub/places"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Config represents backend api credentials for maps & geodata.
@@ -209,7 +209,7 @@ func (c *Config) Refresh() (err error) {
// Load backend api credentials from a YAML file.
func (c *Config) Load() error {
if !fs.FileExists(c.FileName) {
- return fmt.Errorf("settings file not found: %s", sanitize.Log(c.FileName))
+ return fmt.Errorf("settings file not found: %s", clean.Log(c.FileName))
}
mutex.Lock()
diff --git a/internal/hub/places/location.go b/internal/hub/places/location.go
index 52664b49e..59858cbf5 100644
--- a/internal/hub/places/location.go
+++ b/internal/hub/places/location.go
@@ -8,8 +8,8 @@ import (
"strings"
"time"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/s2"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -47,7 +47,7 @@ func FindLocation(id string) (result Location, err error) {
if len(id) == 0 {
return result, fmt.Errorf("empty cell id")
} else if n := len(id); n < 4 || n > 16 {
- return result, fmt.Errorf("invalid cell id %s", sanitize.Log(id))
+ return result, fmt.Errorf("invalid cell id %s", clean.Log(id))
}
// Remember start time.
@@ -145,7 +145,7 @@ func FindLocation(id string) (result Location, err error) {
}
cache.SetDefault(id, result)
- log.Tracef("places: cached cell %s [%s]", sanitize.Log(id), time.Since(start))
+ log.Tracef("places: cached cell %s [%s]", clean.Log(id), time.Since(start))
result.Cached = false
@@ -204,7 +204,7 @@ func (l Location) CountryCode() (result string) {
// State returns the location address state name.
func (l Location) State() (result string) {
- return sanitize.State(l.Place.LocState, l.CountryCode())
+ return clean.State(l.Place.LocState, l.CountryCode())
}
// Latitude returns the location position latitude.
diff --git a/internal/maps/location.go b/internal/maps/location.go
index 87bac3912..320ee2e61 100644
--- a/internal/maps/location.go
+++ b/internal/maps/location.go
@@ -5,8 +5,8 @@ import (
"strings"
"github.com/photoprism/photoprism/internal/hub/places"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/s2"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -123,7 +123,7 @@ func (l Location) CountryCode() string {
}
func (l Location) State() string {
- return txt.Clip(sanitize.State(l.LocState, l.CountryCode()), 100)
+ return txt.Clip(clean.State(l.LocState, l.CountryCode()), 100)
}
func (l Location) CountryName() string {
diff --git a/internal/meta/codec.go b/internal/meta/codec.go
index f77eb94e3..bc0b39ab8 100644
--- a/internal/meta/codec.go
+++ b/internal/meta/codec.go
@@ -1,6 +1,8 @@
package meta
-import "github.com/photoprism/photoprism/internal/video"
+import (
+ "github.com/photoprism/photoprism/pkg/video"
+)
const CodecUnknown = ""
const CodecAvc1 = string(video.CodecAVC)
diff --git a/internal/meta/data.go b/internal/meta/data.go
index da4a8c0db..38c189ce7 100644
--- a/internal/meta/data.go
+++ b/internal/meta/data.go
@@ -100,12 +100,12 @@ func (data Data) Megapixels() int {
// HasDocumentID returns true if a DocumentID exists.
func (data Data) HasDocumentID() bool {
- return rnd.IsUUID(data.DocumentID)
+ return rnd.ValidUUID(data.DocumentID)
}
// HasInstanceID returns true if an InstanceID exists.
func (data Data) HasInstanceID() bool {
- return rnd.IsUUID(data.InstanceID)
+ return rnd.ValidUUID(data.InstanceID)
}
// HasTimeAndPlace if data contains a time and GPS position.
diff --git a/internal/meta/report.go b/internal/meta/data_report.go
similarity index 84%
rename from internal/meta/report.go
rename to internal/meta/data_report.go
index 8c99b50e0..0b2011ddf 100644
--- a/internal/meta/report.go
+++ b/internal/meta/data_report.go
@@ -5,7 +5,11 @@ import (
"strings"
"time"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/projection"
+
+ "github.com/photoprism/photoprism/pkg/media"
+
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Report returns form fields as table rows for reports.
@@ -43,7 +47,9 @@ func Report(f interface{}) (rows [][]string, cols []string) {
switch t := fieldValue.Interface().(type) {
case Keywords:
- typeName = "keywords"
+ typeName = "list"
+ case projection.Type, media.Type:
+ typeName = "type"
case time.Duration:
typeName = "duration"
case time.Time:
@@ -59,7 +65,7 @@ func Report(f interface{}) (rows [][]string, cols []string) {
case bool:
typeName = "flag"
default:
- log.Warnf("failed reporting on %T %s", t, sanitize.Token(fieldName))
+ log.Warnf("failed reporting on %T %s", t, clean.Token(fieldName))
continue
}
diff --git a/internal/meta/exif.go b/internal/meta/exif.go
index 84091ab0b..de0456a17 100644
--- a/internal/meta/exif.go
+++ b/internal/meta/exif.go
@@ -15,9 +15,10 @@ import (
exifcommon "github.com/dsoprea/go-exif/v3/common"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
+ "github.com/photoprism/photoprism/pkg/projection"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -36,20 +37,20 @@ func init() {
}
// Exif parses an image file for Exif metadata and returns as Data struct.
-func Exif(fileName string, fileType fs.Format, bruteForce bool) (data Data, err error) {
+func Exif(fileName string, fileType fs.Type, bruteForce bool) (data Data, err error) {
err = data.Exif(fileName, fileType, bruteForce)
return data, err
}
// Exif parses an image file for Exif metadata and returns as Data struct.
-func (data *Data) Exif(fileName string, fileFormat fs.Format, bruteForce bool) (err error) {
+func (data *Data) Exif(fileName string, fileFormat fs.Type, bruteForce bool) (err error) {
exifMutex.Lock()
defer exifMutex.Unlock()
defer func() {
if e := recover(); e != nil {
- err = fmt.Errorf("metadata: %s in %s (exif panic)\nstack: %s", e, sanitize.Log(filepath.Base(fileName)), debug.Stack())
+ err = fmt.Errorf("metadata: %s in %s (exif panic)\nstack: %s", e, clean.Log(filepath.Base(fileName)), debug.Stack())
}
}()
@@ -60,7 +61,7 @@ func (data *Data) Exif(fileName string, fileFormat fs.Format, bruteForce bool) (
return err
}
- logName := sanitize.Log(filepath.Base(fileName))
+ logName := clean.Log(filepath.Base(fileName))
// Enumerate data.exif in Exif block.
opt := exif.ScanOptions{}
@@ -102,7 +103,7 @@ func (data *Data) Exif(fileName string, fileFormat fs.Format, bruteForce bool) (
data.Lat = float32(gi.Latitude.Decimal())
data.Lng = float32(gi.Longitude.Decimal())
} else if gi.Altitude != 0 || !gi.Timestamp.IsZero() {
- log.Warnf("metadata: invalid exif gps coordinates in %s (%s)", logName, sanitize.Log(gi.String()))
+ log.Warnf("metadata: invalid exif gps coordinates in %s (%s)", logName, clean.Log(gi.String()))
}
if gi.Altitude != 0 {
@@ -281,7 +282,7 @@ func (data *Data) Exif(fileName string, fileFormat fs.Format, bruteForce bool) (
}
}
- // Valid time found in Exif metadata?
+ // UniqueID time found in Exif metadata?
if !takenAt.IsZero() {
if takenAtLocal, err := time.ParseInLocation("2006-01-02T15:04:05", takenAt.Format("2006-01-02T15:04:05"), time.UTC); err == nil {
data.TakenAtLocal = takenAtLocal
@@ -314,7 +315,7 @@ func (data *Data) Exif(fileName string, fileFormat fs.Format, bruteForce bool) (
if value, ok := data.exif["ProjectionType"]; ok {
data.AddKeywords(KeywordPanorama)
- data.Projection = SanitizeString(value)
+ data.Projection = projection.New(SanitizeString(value)).String()
}
data.Subject = SanitizeMeta(data.Subject)
diff --git a/internal/meta/exif_parser.go b/internal/meta/exif_parser.go
index a31f5dbd5..72223f680 100644
--- a/internal/meta/exif_parser.go
+++ b/internal/meta/exif_parser.go
@@ -12,14 +12,14 @@ import (
pngstructure "github.com/dsoprea/go-png-image-structure/v2"
tiffstructure "github.com/dsoprea/go-tiff-image-structure/v2"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
-func RawExif(fileName string, fileFormat fs.Format, bruteForce bool) (rawExif []byte, err error) {
+func RawExif(fileName string, fileFormat fs.Type, bruteForce bool) (rawExif []byte, err error) {
defer func() {
if e := recover(); e != nil {
- err = fmt.Errorf("%s in %s (raw exif panic)\nstack: %s", e, sanitize.Log(filepath.Base(fileName)), debug.Stack())
+ err = fmt.Errorf("%s in %s (raw exif panic)\nstack: %s", e, clean.Log(filepath.Base(fileName)), debug.Stack())
}
}()
@@ -27,11 +27,11 @@ func RawExif(fileName string, fileFormat fs.Format, bruteForce bool) (rawExif []
var parsed bool
// Sanitized and shortened file name for logs.
- logName := sanitize.Log(filepath.Base(fileName))
+ logName := clean.Log(filepath.Base(fileName))
// Try Exif parser for specific media file format first.
switch fileFormat {
- case fs.FormatJpeg:
+ case fs.ImageJPEG:
jpegMp := jpegstructure.NewJpegMediaParser()
sl, err := jpegMp.ParseFile(fileName)
@@ -53,7 +53,7 @@ func RawExif(fileName string, fileFormat fs.Format, bruteForce bool) (rawExif []
parsed = true
}
}
- case fs.FormatPng:
+ case fs.ImagePNG:
pngMp := pngstructure.NewPngMediaParser()
cs, err := pngMp.ParseFile(fileName)
@@ -73,7 +73,7 @@ func RawExif(fileName string, fileFormat fs.Format, bruteForce bool) (rawExif []
parsed = true
}
}
- case fs.FormatHEIF:
+ case fs.ImageHEIF:
heicMp := heicexif.NewHeicExifMediaParser()
cs, err := heicMp.ParseFile(fileName)
@@ -93,7 +93,7 @@ func RawExif(fileName string, fileFormat fs.Format, bruteForce bool) (rawExif []
parsed = true
}
}
- case fs.FormatTiff:
+ case fs.ImageTIFF:
tiffMp := tiffstructure.NewTiffMediaParser()
cs, err := tiffMp.ParseFile(fileName)
diff --git a/internal/meta/exif_test.go b/internal/meta/exif_test.go
index 848c8128e..75766a7b1 100644
--- a/internal/meta/exif_test.go
+++ b/internal/meta/exif_test.go
@@ -3,13 +3,14 @@ package meta
import (
"testing"
- "github.com/photoprism/photoprism/pkg/fs"
"github.com/stretchr/testify/assert"
+
+ "github.com/photoprism/photoprism/pkg/fs"
)
func TestExif(t *testing.T) {
t.Run("iptc-2014.jpg", func(t *testing.T) {
- data, err := Exif("testdata/iptc-2014.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/iptc-2014.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -37,7 +38,7 @@ func TestExif(t *testing.T) {
})
t.Run("iptc-2016.jpg", func(t *testing.T) {
- data, err := Exif("testdata/iptc-2016.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/iptc-2016.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -65,7 +66,7 @@ func TestExif(t *testing.T) {
})
t.Run("photoshop.jpg", func(t *testing.T) {
- data, err := Exif("testdata/photoshop.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/photoshop.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -98,7 +99,7 @@ func TestExif(t *testing.T) {
})
t.Run("ladybug.jpg", func(t *testing.T) {
- data, err := Exif("testdata/ladybug.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/ladybug.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -131,7 +132,7 @@ func TestExif(t *testing.T) {
})
t.Run("gopro_hd2.jpg", func(t *testing.T) {
- data, err := Exif("testdata/gopro_hd2.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/gopro_hd2.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -161,7 +162,7 @@ func TestExif(t *testing.T) {
})
t.Run("tweethog.png", func(t *testing.T) {
- _, err := Exif("testdata/tweethog.png", fs.FormatPng, true)
+ _, err := Exif("testdata/tweethog.png", fs.ImagePNG, true)
if err == nil {
t.Fatal("err should NOT be nil")
@@ -171,7 +172,7 @@ func TestExif(t *testing.T) {
})
t.Run("iphone_7.heic", func(t *testing.T) {
- data, err := Exif("testdata/iphone_7.heic", fs.FormatHEIF, true)
+ data, err := Exif("testdata/iphone_7.heic", fs.ImageHEIF, true)
if err != nil {
t.Fatal(err)
}
@@ -192,7 +193,7 @@ func TestExif(t *testing.T) {
})
t.Run("gps-2000.jpg", func(t *testing.T) {
- data, err := Exif("testdata/gps-2000.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/gps-2000.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -220,7 +221,7 @@ func TestExif(t *testing.T) {
})
t.Run("image-2011.jpg", func(t *testing.T) {
- data, err := Exif("testdata/image-2011.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/image-2011.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -257,7 +258,7 @@ func TestExif(t *testing.T) {
})
t.Run("ship.jpg", func(t *testing.T) {
- data, err := Exif("testdata/ship.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/ship.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -279,7 +280,7 @@ func TestExif(t *testing.T) {
})
t.Run("no-exif-data.jpg", func(t *testing.T) {
- _, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg, false)
+ _, err := Exif("testdata/no-exif-data.jpg", fs.ImageJPEG, false)
if err == nil {
t.Fatal("err should NOT be nil")
@@ -289,7 +290,7 @@ func TestExif(t *testing.T) {
})
t.Run("no-exif-data.jpg/BruteForce", func(t *testing.T) {
- _, err := Exif("testdata/no-exif-data.jpg", fs.FormatJpeg, true)
+ _, err := Exif("testdata/no-exif-data.jpg", fs.ImageJPEG, true)
if err == nil {
t.Fatal("err should NOT be nil")
@@ -299,7 +300,7 @@ func TestExif(t *testing.T) {
})
t.Run("screenshot.png", func(t *testing.T) {
- data, err := Exif("testdata/screenshot.png", fs.FormatPng, true)
+ data, err := Exif("testdata/screenshot.png", fs.ImagePNG, true)
if err != nil {
t.Fatal(err)
@@ -310,7 +311,7 @@ func TestExif(t *testing.T) {
})
t.Run("orientation.jpg", func(t *testing.T) {
- data, err := Exif("testdata/orientation.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/orientation.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -338,19 +339,19 @@ func TestExif(t *testing.T) {
})
t.Run("gopher-preview.jpg", func(t *testing.T) {
- _, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg, false)
+ _, err := Exif("testdata/gopher-preview.jpg", fs.ImageJPEG, false)
assert.EqualError(t, err, "found no exif header")
})
t.Run("gopher-preview.jpg/BruteForce", func(t *testing.T) {
- _, err := Exif("testdata/gopher-preview.jpg", fs.FormatJpeg, true)
+ _, err := Exif("testdata/gopher-preview.jpg", fs.ImageJPEG, true)
assert.EqualError(t, err, "found no exif data")
})
t.Run("huawei-gps-error.jpg", func(t *testing.T) {
- data, err := Exif("testdata/huawei-gps-error.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/huawei-gps-error.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -372,7 +373,7 @@ func TestExif(t *testing.T) {
})
t.Run("panorama360.jpg", func(t *testing.T) {
- data, err := Exif("testdata/panorama360.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/panorama360.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -404,7 +405,7 @@ func TestExif(t *testing.T) {
})
t.Run("exif-example.tiff", func(t *testing.T) {
- data, err := Exif("testdata/exif-example.tiff", fs.FormatTiff, true)
+ data, err := Exif("testdata/exif-example.tiff", fs.ImageTIFF, true)
if err != nil {
t.Fatal(err)
@@ -436,7 +437,7 @@ func TestExif(t *testing.T) {
})
t.Run("out-of-range-500.jpg", func(t *testing.T) {
- data, err := Exif("testdata/out-of-range-500.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/out-of-range-500.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -468,7 +469,7 @@ func TestExif(t *testing.T) {
})
t.Run("digikam.jpg", func(t *testing.T) {
- data, err := Exif("testdata/digikam.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/digikam.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -503,7 +504,7 @@ func TestExif(t *testing.T) {
})
t.Run("notebook.jpg", func(t *testing.T) {
- data, err := Exif("testdata/notebook.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/notebook.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -524,7 +525,7 @@ func TestExif(t *testing.T) {
})
t.Run("snow.jpg", func(t *testing.T) {
- data, err := Exif("testdata/snow.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/snow.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -545,7 +546,7 @@ func TestExif(t *testing.T) {
})
t.Run("keywords.jpg", func(t *testing.T) {
- data, err := Exif("testdata/keywords.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/keywords.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -565,7 +566,7 @@ func TestExif(t *testing.T) {
})
t.Run("Iceland-P3.jpg", func(t *testing.T) {
- data, err := Exif("testdata/Iceland-P3.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/Iceland-P3.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -597,7 +598,7 @@ func TestExif(t *testing.T) {
})
t.Run("Iceland-sRGB.jpg", func(t *testing.T) {
- data, err := Exif("testdata/Iceland-sRGB.jpg", fs.FormatJpeg, true)
+ data, err := Exif("testdata/Iceland-sRGB.jpg", fs.ImageJPEG, true)
if err != nil {
t.Fatal(err)
@@ -628,7 +629,7 @@ func TestExif(t *testing.T) {
assert.Equal(t, "", data.ColorProfile)
})
t.Run("animated.gif", func(t *testing.T) {
- _, err := Exif("testdata/animated.gif", fs.FormatGif, true)
+ _, err := Exif("testdata/animated.gif", fs.ImageGIF, true)
if err == nil {
t.Fatal("error expected")
diff --git a/internal/meta/gps.go b/internal/meta/gps.go
index 0ef8dc984..a6f7ec843 100644
--- a/internal/meta/gps.go
+++ b/internal/meta/gps.go
@@ -4,7 +4,7 @@ import (
"regexp"
"strconv"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/dsoprea/go-exif/v3"
)
@@ -23,7 +23,7 @@ func GpsToLatLng(s string) (lat, lng float32) {
// Floating point numbers?
if fl := GpsFloatRegexp.FindAllString(s, -1); len(fl) == 2 {
if lat, err := strconv.ParseFloat(fl[0], 64); err != nil {
- log.Infof("metadata: %s is not a valid gps position", sanitize.Log(fl[0]))
+ log.Infof("metadata: %s is not a valid gps position", clean.Log(fl[0]))
} else if lng, err := strconv.ParseFloat(fl[1], 64); err == nil {
return float32(lat), float32(lng)
}
@@ -93,7 +93,7 @@ func ParseFloat(s string) float64 {
// Parse floating point number.
if result, err := strconv.ParseFloat(s, 64); err != nil {
- log.Debugf("metadata: %s is not a valid gps position", sanitize.Log(s))
+ log.Debugf("metadata: %s is not a valid gps position", clean.Log(s))
return 0
} else {
return result
diff --git a/internal/meta/json.go b/internal/meta/json.go
index 0ec9f4e4a..3750a7e38 100644
--- a/internal/meta/json.go
+++ b/internal/meta/json.go
@@ -7,8 +7,8 @@ import (
"path/filepath"
"runtime/debug"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// JSON parses a json sidecar file (as used by Exiftool) and returns a Data struct.
@@ -22,11 +22,11 @@ func JSON(jsonName, originalName string) (data Data, err error) {
func (data *Data) JSON(jsonName, originalName string) (err error) {
defer func() {
if e := recover(); e != nil {
- err = fmt.Errorf("metadata: %s in %s (json panic)\nstack: %s", e, sanitize.Log(filepath.Base(jsonName)), debug.Stack())
+ err = fmt.Errorf("metadata: %s in %s (json panic)\nstack: %s", e, clean.Log(filepath.Base(jsonName)), debug.Stack())
}
}()
- quotedName := sanitize.Log(filepath.Base(jsonName))
+ quotedName := clean.Log(filepath.Base(jsonName))
if !fs.FileExists(jsonName) {
return fmt.Errorf("metadata: %s not found", quotedName)
diff --git a/internal/meta/json_exiftool.go b/internal/meta/json_exiftool.go
index 6ed59e19f..75ba45f9f 100644
--- a/internal/meta/json_exiftool.go
+++ b/internal/meta/json_exiftool.go
@@ -9,8 +9,10 @@ import (
"strings"
"time"
+ "github.com/photoprism/photoprism/pkg/projection"
+
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/rnd"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/tidwall/gjson"
"gopkg.in/photoprism/go-tz.v2/tz"
@@ -30,7 +32,7 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
j := gjson.GetBytes(jsonData, "@flatten|@join")
if !j.IsObject() {
- return fmt.Errorf("metadata: data is not an object in %s (exiftool)", sanitize.Log(filepath.Base(originalName)))
+ return fmt.Errorf("metadata: data is not an object in %s (exiftool)", clean.Log(filepath.Base(originalName)))
}
jsonStrings := make(map[string]string)
@@ -41,7 +43,7 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
}
if fileName, ok := jsonStrings["FileName"]; ok && fileName != "" && originalName != "" && fileName != originalName {
- return fmt.Errorf("metadata: original name %s does not match %s (exiftool)", sanitize.Log(originalName), sanitize.Log(fileName))
+ return fmt.Errorf("metadata: original name %s does not match %s (exiftool)", clean.Log(originalName), clean.Log(fileName))
}
v := reflect.ValueOf(data).Elem()
@@ -112,6 +114,8 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
case Keywords:
existing := fieldValue.Interface().(Keywords)
fieldValue.Set(reflect.ValueOf(txt.AddToWords(existing, strings.TrimSpace(jsonValue.String()))))
+ case projection.Type:
+ fieldValue.Set(reflect.ValueOf(projection.Type(strings.TrimSpace(jsonValue.String()))))
case string:
if !fieldValue.IsZero() {
continue
@@ -288,7 +292,7 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
data.InstanceID = rnd.SanitizeUUID(data.InstanceID)
}
- if data.Projection == "equirectangular" {
+ if projection.Equirectangular.Equal(data.Projection) {
data.AddKeywords(KeywordPanorama)
}
diff --git a/internal/meta/json_test.go b/internal/meta/json_test.go
index f3ecdc19c..06264b8c1 100644
--- a/internal/meta/json_test.go
+++ b/internal/meta/json_test.go
@@ -4,7 +4,10 @@ import (
"testing"
"time"
- "github.com/photoprism/photoprism/internal/video"
+ "github.com/photoprism/photoprism/pkg/video"
+
+ "github.com/photoprism/photoprism/pkg/projection"
+
"github.com/stretchr/testify/assert"
)
@@ -467,7 +470,7 @@ func TestJSON(t *testing.T) {
assert.Equal(t, "", data.CameraSerial)
assert.Equal(t, 0, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
- assert.Equal(t, "equirectangular", data.Projection)
+ assert.Equal(t, projection.Equirectangular.String(), data.Projection)
})
t.Run("P7250006.json", func(t *testing.T) {
diff --git a/internal/meta/keywords.go b/internal/meta/keywords.go
index f96208459..e49dce21c 100644
--- a/internal/meta/keywords.go
+++ b/internal/meta/keywords.go
@@ -3,6 +3,8 @@ package meta
import (
"strings"
+ "github.com/photoprism/photoprism/pkg/projection"
+
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -11,7 +13,7 @@ const (
KeywordHdr = "hdr"
KeywordBurst = "burst"
KeywordPanorama = "panorama"
- KeywordEquirectangular = "equirectangular"
+ KeywordEquirectangular = string(projection.Equirectangular)
)
// Keywords represents a list of metadata keywords.
diff --git a/internal/meta/xmp.go b/internal/meta/xmp.go
index 1e906c595..6cf6156c8 100644
--- a/internal/meta/xmp.go
+++ b/internal/meta/xmp.go
@@ -5,7 +5,7 @@ import (
"path/filepath"
"runtime/debug"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// XMP parses an XMP file and returns a Data struct.
@@ -19,14 +19,14 @@ func XMP(fileName string) (data Data, err error) {
func (data *Data) XMP(fileName string) (err error) {
defer func() {
if e := recover(); e != nil {
- err = fmt.Errorf("metadata: %s in %s (xmp panic)\nstack: %s", e, sanitize.Log(filepath.Base(fileName)), debug.Stack())
+ err = fmt.Errorf("metadata: %s in %s (xmp panic)\nstack: %s", e, clean.Log(filepath.Base(fileName)), debug.Stack())
}
}()
doc := XmpDocument{}
if err := doc.Load(fileName); err != nil {
- return fmt.Errorf("metadata: cannot read %s (xmp)", sanitize.Log(filepath.Base(fileName)))
+ return fmt.Errorf("metadata: cannot read %s (xmp)", clean.Log(filepath.Base(fileName)))
}
if doc.Title() != "" {
diff --git a/internal/nsfw/detector.go b/internal/nsfw/detector.go
index 188e3948c..910b0bf30 100644
--- a/internal/nsfw/detector.go
+++ b/internal/nsfw/detector.go
@@ -7,8 +7,8 @@ import (
"path/filepath"
"sync"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
tf "github.com/tensorflow/tensorflow/tensorflow/go"
"github.com/tensorflow/tensorflow/tensorflow/go/op"
)
@@ -30,7 +30,7 @@ func New(modelPath string) *Detector {
// File returns matching labels for a jpeg media file.
func (t *Detector) File(filename string) (result Labels, err error) {
if fs.MimeType(filename) != "image/jpeg" {
- return result, fmt.Errorf("nsfw: %s is not a jpeg file", sanitize.Log(filepath.Base(filename)))
+ return result, fmt.Errorf("nsfw: %s is not a jpeg file", clean.Log(filepath.Base(filename)))
}
imageBuffer, err := os.ReadFile(filename)
@@ -118,7 +118,7 @@ func (t *Detector) loadModel() error {
return nil
}
- log.Infof("nsfw: loading %s", sanitize.Log(filepath.Base(t.modelPath)))
+ log.Infof("nsfw: loading %s", clean.Log(filepath.Base(t.modelPath)))
// Load model
model, err := tf.LoadSavedModel(t.modelPath, t.modelTags, nil)
diff --git a/internal/photoprism/albums.go b/internal/photoprism/albums.go
index f853c29c4..ff433714a 100644
--- a/internal/photoprism/albums.go
+++ b/internal/photoprism/albums.go
@@ -6,8 +6,8 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// BackupAlbums creates a YAML file backup of all albums.
@@ -36,7 +36,7 @@ func BackupAlbums(backupPath string, force bool) (count int, result error) {
log.Errorf("album: %s (update yaml)", err)
result = err
} else {
- log.Tracef("backup: saved album yaml file %s", sanitize.Log(filepath.Base(fileName)))
+ log.Tracef("backup: saved album yaml file %s", clean.Log(filepath.Base(fileName)))
count++
}
}
@@ -87,14 +87,14 @@ func RestoreAlbums(backupPath string, force bool) (count int, result error) {
a := entity.Album{}
if err := a.LoadFromYaml(fileName); err != nil {
- log.Errorf("restore: %s in %s", err, sanitize.Log(filepath.Base(fileName)))
+ log.Errorf("restore: %s in %s", err, clean.Log(filepath.Base(fileName)))
result = err
} else if a.AlbumType == "" || len(a.Photos) == 0 && a.AlbumFilter == "" {
- log.Debugf("restore: skipping %s", sanitize.Log(filepath.Base(fileName)))
+ log.Debugf("restore: skipping %s", clean.Log(filepath.Base(fileName)))
} else if err := a.Find(); err == nil {
- log.Infof("%s: %s already exists", a.AlbumType, sanitize.Log(a.AlbumTitle))
+ log.Infof("%s: %s already exists", a.AlbumType, clean.Log(a.AlbumTitle))
} else if err := a.Create(); err != nil {
- log.Errorf("%s: %s in %s", a.AlbumType, err, sanitize.Log(filepath.Base(fileName)))
+ log.Errorf("%s: %s in %s", a.AlbumType, err, clean.Log(filepath.Base(fileName)))
} else {
count++
}
diff --git a/internal/photoprism/cleanup.go b/internal/photoprism/cleanup.go
index c922e7b1b..8240ce261 100644
--- a/internal/photoprism/cleanup.go
+++ b/internal/photoprism/cleanup.go
@@ -15,9 +15,9 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fastwalk"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// CleanUp represents a worker that deletes unneeded data and files.
@@ -82,7 +82,7 @@ func (w *CleanUp) Start(opt CleanUpOptions) (thumbs int, orphans int, err error)
}
hash := base[:i]
- logName := sanitize.Log(fs.RelName(fileName, thumbPath))
+ logName := clean.Log(fs.RelName(fileName, thumbPath))
if ok := fileHashes[hash]; ok {
// Do nothing.
@@ -119,7 +119,7 @@ func (w *CleanUp) Start(opt CleanUpOptions) (thumbs int, orphans int, err error)
if opt.Dry {
orphans++
- log.Infof("cleanup: orphan photo %s would be removed", sanitize.Log(p.PhotoUID))
+ log.Infof("cleanup: orphan photo %s would be removed", clean.Log(p.PhotoUID))
continue
}
diff --git a/internal/photoprism/colors.go b/internal/photoprism/colors.go
index 7bab280f5..4a2a91e3d 100644
--- a/internal/photoprism/colors.go
+++ b/internal/photoprism/colors.go
@@ -8,20 +8,20 @@ import (
"github.com/lucasb-eyer/go-colorful"
"github.com/photoprism/photoprism/internal/thumb"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/colors"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Colors returns the ColorPerception of an image (only JPEG supported).
func (m *MediaFile) Colors(thumbPath string) (perception colors.ColorPerception, err error) {
if !m.IsJpeg() {
- return perception, fmt.Errorf("%s is not a jpeg", sanitize.Log(m.BaseName()))
+ return perception, fmt.Errorf("%s is not a jpeg", clean.Log(m.BaseName()))
}
img, err := m.Resample(thumbPath, thumb.Colors)
if err != nil {
- log.Debugf("colors: %s in %s (resample)", err, sanitize.Log(m.BaseName()))
+ log.Debugf("colors: %s in %s (resample)", err, clean.Log(m.BaseName()))
return perception, err
}
diff --git a/internal/photoprism/convert.go b/internal/photoprism/convert.go
index 17ca2272d..1ca1f6f56 100644
--- a/internal/photoprism/convert.go
+++ b/internal/photoprism/convert.go
@@ -12,8 +12,8 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/mutex"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Convert represents a converter that can convert RAW/HEIF images to JPEG.
@@ -71,7 +71,7 @@ func (c *Convert) Start(path string, force bool) (err error) {
}
ignore.Log = func(fileName string) {
- log.Infof("convert: ignoring %s", sanitize.Log(filepath.Base(fileName)))
+ log.Infof("convert: ignoring %s", clean.Log(filepath.Base(fileName)))
}
err = godirwalk.Walk(path, &godirwalk.Options{
diff --git a/internal/photoprism/convert_avc.go b/internal/photoprism/convert_avc.go
index 3bfae4201..e19c4852b 100644
--- a/internal/photoprism/convert_avc.go
+++ b/internal/photoprism/convert_avc.go
@@ -13,8 +13,8 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/ffmpeg"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// ToAvc converts a single video file to MPEG-4 AVC.
@@ -27,7 +27,7 @@ func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
return nil, fmt.Errorf("convert: %s not found", f.RootRelName())
}
- avcName := fs.FormatAVC.FindFirst(f.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
+ avcName := fs.VideoAVC.FindFirst(f.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
mediaFile, err := NewMediaFile(avcName)
@@ -36,7 +36,7 @@ func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
}
if !c.conf.SidecarWritable() {
- return nil, fmt.Errorf("convert: transcoding disabled in read only mode (%s)", f.RootRelName())
+ return nil, fmt.Errorf("convert: transcoding disabled in read-only mode (%s)", f.RootRelName())
}
if c.conf.DisableFFmpeg() {
@@ -44,7 +44,7 @@ func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
}
fileName := f.RelName(c.conf.OriginalsPath())
- avcName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.AvcExt)
+ avcName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.ExtAVC)
cmd, useMutex, err := c.AvcConvertCommand(f, avcName, encoder)
@@ -66,9 +66,9 @@ func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
} else if !force || !avcFile.InSidecar() {
return avcFile, nil
} else if err = avcFile.Remove(); err != nil {
- return avcFile, fmt.Errorf("convert: failed removing %s (%s)", sanitize.Log(avcFile.RootRelName()), err)
+ return avcFile, fmt.Errorf("convert: failed removing %s (%s)", clean.Log(avcFile.RootRelName()), err)
} else {
- log.Infof("convert: replacing %s", sanitize.Log(avcFile.RootRelName()))
+ log.Infof("convert: replacing %s", clean.Log(avcFile.RootRelName()))
}
}
@@ -85,7 +85,7 @@ func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
"xmpName": "",
})
- log.Infof("%s: transcoding %s to %s", encoder, fileName, fs.FormatAVC)
+ log.Infof("%s: transcoding %s to %s", encoder, fileName, fs.VideoAVC)
// Log exact command for debugging in trace mode.
log.Trace(cmd.String())
@@ -109,7 +109,7 @@ func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force
if !fs.FileExists(avcName) {
// Do nothing.
} else if err = os.Remove(avcName); err != nil {
- return nil, fmt.Errorf("convert: failed removing %s (%s)", sanitize.Log(RootRelName(avcName)), err)
+ return nil, fmt.Errorf("convert: failed removing %s (%s)", clean.Log(RootRelName(avcName)), err)
}
// Try again using software encoder.
@@ -138,9 +138,9 @@ func (c *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg
case bitrate == "":
return nil, false, fmt.Errorf("convert: transcoding bitrate is empty - possible bug")
case ffmpegBin == "":
- return nil, false, fmt.Errorf("convert: ffmpeg must be installed to transcode %s to avc", sanitize.Log(f.BaseName()))
+ return nil, false, fmt.Errorf("convert: ffmpeg must be installed to transcode %s to avc", clean.Log(f.BaseName()))
case !f.IsAnimated():
- return nil, false, fmt.Errorf("convert: file type %s of %s cannot be transcoded to avc", f.FileType(), sanitize.Log(f.BaseName()))
+ return nil, false, fmt.Errorf("convert: file type %s of %s cannot be transcoded to avc", f.FileType(), clean.Log(f.BaseName()))
}
return ffmpeg.AvcConvertCommand(fileName, avcName, ffmpegBin, c.AvcBitrate(f), encoder)
diff --git a/internal/photoprism/convert_jpeg.go b/internal/photoprism/convert_jpeg.go
index a3b77750e..de966a5ce 100644
--- a/internal/photoprism/convert_jpeg.go
+++ b/internal/photoprism/convert_jpeg.go
@@ -12,8 +12,8 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/thumb"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// ToJpeg converts a single image file to JPEG if possible.
@@ -23,14 +23,14 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
}
if !f.Exists() {
- return nil, fmt.Errorf("convert: %s not found", sanitize.Log(f.RootRelName()))
+ return nil, fmt.Errorf("convert: %s not found", clean.Log(f.RootRelName()))
}
if f.IsJpeg() {
return f, nil
}
- jpegName := fs.FormatJpeg.FindFirst(f.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
+ jpegName := fs.ImageJPEG.FindFirst(f.FileName(), []string{c.conf.SidecarPath(), fs.HiddenPath}, c.conf.OriginalsPath(), false)
mediaFile, err := NewMediaFile(jpegName)
@@ -38,23 +38,23 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
if err == nil && mediaFile.IsJpeg() {
if force && mediaFile.InSidecar() {
if err := mediaFile.Remove(); err != nil {
- return mediaFile, fmt.Errorf("convert: failed removing %s (%s)", sanitize.Log(mediaFile.RootRelName()), err)
+ return mediaFile, fmt.Errorf("convert: failed removing %s (%s)", clean.Log(mediaFile.RootRelName()), err)
} else {
- log.Infof("convert: replacing %s", sanitize.Log(mediaFile.RootRelName()))
+ log.Infof("convert: replacing %s", clean.Log(mediaFile.RootRelName()))
}
} else {
return mediaFile, nil
}
} else {
- jpegName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.JpegExt)
+ jpegName = fs.FileName(f.FileName(), c.conf.SidecarPath(), c.conf.OriginalsPath(), fs.ExtJPEG)
}
if !c.conf.SidecarWritable() {
- return nil, fmt.Errorf("convert: disabled in read only mode (%s)", sanitize.Log(f.RootRelName()))
+ return nil, fmt.Errorf("convert: disabled in read-only mode (%s)", clean.Log(f.RootRelName()))
}
fileName := f.RelName(c.conf.OriginalsPath())
- xmpName := fs.FormatXMP.Find(f.FileName(), false)
+ xmpName := fs.XmpFile.Find(f.FileName(), false)
event.Publish("index.converting", event.Data{
"fileType": f.FileType(),
@@ -66,7 +66,7 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
start := time.Now()
if f.IsImageOther() {
- log.Infof("convert: converting %s to %s (%s)", sanitize.Log(filepath.Base(fileName)), sanitize.Log(filepath.Base(jpegName)), f.FileType())
+ log.Infof("convert: converting %s to %s (%s)", clean.Log(filepath.Base(fileName)), clean.Log(filepath.Base(jpegName)), f.FileType())
_, err = thumb.Jpeg(f.FileName(), jpegName, f.Orientation())
@@ -74,7 +74,7 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
return nil, err
}
- log.Infof("convert: %s created in %s (%s)", sanitize.Log(filepath.Base(jpegName)), time.Since(start), f.FileType())
+ log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(jpegName)), time.Since(start), f.FileType())
return NewMediaFile(jpegName)
}
@@ -102,7 +102,7 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
cmd.Stdout = &out
cmd.Stderr = &stderr
- log.Infof("convert: converting %s to %s (%s)", sanitize.Log(filepath.Base(fileName)), sanitize.Log(filepath.Base(jpegName)), filepath.Base(cmd.Path))
+ log.Infof("convert: converting %s to %s (%s)", clean.Log(filepath.Base(fileName)), clean.Log(filepath.Base(jpegName)), filepath.Base(cmd.Path))
// Log exact command for debugging in trace mode.
log.Trace(cmd.String())
@@ -116,7 +116,7 @@ func (c *Convert) ToJpeg(f *MediaFile, force bool) (*MediaFile, error) {
}
}
- log.Infof("convert: %s created in %s (%s)", sanitize.Log(filepath.Base(jpegName)), time.Since(start), filepath.Base(cmd.Path))
+ log.Infof("convert: %s created in %s (%s)", clean.Log(filepath.Base(jpegName)), time.Since(start), filepath.Base(cmd.Path))
return NewMediaFile(jpegName)
}
diff --git a/internal/photoprism/convert_json.go b/internal/photoprism/convert_json.go
index cc36d2e25..3abfde330 100644
--- a/internal/photoprism/convert_json.go
+++ b/internal/photoprism/convert_json.go
@@ -8,8 +8,8 @@ import (
"os/exec"
"path/filepath"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// ToJson uses exiftool to export metadata to a json file.
@@ -28,7 +28,7 @@ func (c *Convert) ToJson(f *MediaFile) (jsonName string, err error) {
return jsonName, nil
}
- log.Debugf("exiftool: extracting metadata from %s", sanitize.Log(f.RootRelName()))
+ log.Debugf("exiftool: extracting metadata from %s", clean.Log(f.RootRelName()))
cmd := exec.Command(c.conf.ExifToolBin(), "-n", "-m", "-api", "LargeFileSupport", "-j", f.FileName())
diff --git a/internal/photoprism/convert_worker.go b/internal/photoprism/convert_worker.go
index af57e4dd7..9503c4428 100644
--- a/internal/photoprism/convert_worker.go
+++ b/internal/photoprism/convert_worker.go
@@ -3,7 +3,7 @@ package photoprism
import (
"strings"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
type ConvertJob struct {
@@ -15,7 +15,7 @@ type ConvertJob struct {
func ConvertWorker(jobs <-chan ConvertJob) {
logError := func(err error, job ConvertJob) {
fileName := job.file.RelName(job.convert.conf.OriginalsPath())
- log.Errorf("convert: %s for %s", strings.TrimSpace(err.Error()), sanitize.Log(fileName))
+ log.Errorf("convert: %s for %s", strings.TrimSpace(err.Error()), clean.Log(fileName))
}
for job := range jobs {
diff --git a/internal/photoprism/delete.go b/internal/photoprism/delete.go
index b146b2e1e..c779ec4b4 100644
--- a/internal/photoprism/delete.go
+++ b/internal/photoprism/delete.go
@@ -5,8 +5,8 @@ import (
"path/filepath"
"github.com/photoprism/photoprism/internal/entity"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Delete permanently removes a photo and all its files.
@@ -24,16 +24,16 @@ func Delete(p entity.Photo) error {
for _, file := range files {
fileName := FileName(file.FileRoot, file.FileName)
- log.Debugf("delete: removing file %s", sanitize.Log(file.FileName))
+ log.Debugf("delete: removing file %s", clean.Log(file.FileName))
if f, err := NewMediaFile(fileName); err == nil {
if sidecarJson := f.SidecarJsonName(); fs.FileExists(sidecarJson) {
- log.Debugf("delete: removing json sidecar %s", sanitize.Log(filepath.Base(sidecarJson)))
+ log.Debugf("delete: removing json sidecar %s", clean.Log(filepath.Base(sidecarJson)))
logWarn("delete", os.Remove(sidecarJson))
}
if exifJson, err := f.ExifToolJsonName(); err == nil && fs.FileExists(exifJson) {
- log.Debugf("delete: removing exiftool sidecar %s", sanitize.Log(filepath.Base(exifJson)))
+ log.Debugf("delete: removing exiftool sidecar %s", clean.Log(filepath.Base(exifJson)))
logWarn("delete", os.Remove(exifJson))
}
@@ -47,7 +47,7 @@ func Delete(p entity.Photo) error {
// Remove sidecar backup.
if fs.FileExists(yamlFileName) {
- log.Debugf("delete: removing yaml sidecar %s", sanitize.Log(filepath.Base(yamlFileName)))
+ log.Debugf("delete: removing yaml sidecar %s", clean.Log(filepath.Base(yamlFileName)))
logWarn("delete", os.Remove(yamlFileName))
}
diff --git a/internal/photoprism/faces_audit.go b/internal/photoprism/faces_audit.go
index 6d858d1b7..38e4f81e3 100644
--- a/internal/photoprism/faces_audit.go
+++ b/internal/photoprism/faces_audit.go
@@ -6,7 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/query"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Audit face clusters and subjects.
@@ -161,7 +161,7 @@ func (w *Faces) Audit(fix bool) (err error) {
} else if m.SubjUID != "" {
log.Infof("faces: marker %s with %s subject %s (%s) conflicts with face %s (%s) of subject %s (%s)", m.MarkerUID, entity.SrcString(m.SubjSrc), entity.SubjNames.Log(m.SubjUID), m.SubjUID, m.FaceID, entity.SrcString(f.FaceSrc), entity.SubjNames.Log(f.SubjUID), f.SubjUID)
} else if m.MarkerName != "" {
- log.Infof("faces: marker %s with %s subject name %s conflicts with face %s (%s) of subject %s (%s)", m.MarkerUID, entity.SrcString(m.SubjSrc), sanitize.Log(m.MarkerName), m.FaceID, entity.SrcString(f.FaceSrc), entity.SubjNames.Log(f.SubjUID), f.SubjUID)
+ log.Infof("faces: marker %s with %s subject name %s conflicts with face %s (%s) of subject %s (%s)", m.MarkerUID, entity.SrcString(m.SubjSrc), clean.Log(m.MarkerName), m.FaceID, entity.SrcString(f.FaceSrc), entity.SubjNames.Log(f.SubjUID), f.SubjUID)
} else {
log.Infof("faces: marker %s with unknown subject (%s) conflicts with face %s (%s) of subject %s (%s)", m.MarkerUID, entity.SrcString(m.SubjSrc), m.FaceID, entity.SrcString(f.FaceSrc), entity.SubjNames.Log(f.SubjUID), f.SubjUID)
}
diff --git a/internal/photoprism/import.go b/internal/photoprism/import.go
index 74f73f961..e83d77373 100644
--- a/internal/photoprism/import.go
+++ b/internal/photoprism/import.go
@@ -11,14 +11,16 @@ import (
"strings"
"sync"
+ "github.com/photoprism/photoprism/pkg/media"
+
"github.com/karrick/godirwalk"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/mutex"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Import represents an importer that can copy/move MediaFiles to the originals directory.
@@ -150,7 +152,7 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
done[fileName] = fs.Found
- if !fs.IsMedia(fileName) {
+ if !media.MainFile(fileName) {
return nil
}
@@ -164,7 +166,7 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
// Ignore RAW images?
if mf.IsRaw() && skipRaw {
- log.Infof("import: skipped raw %s", sanitize.Log(mf.RootRelName()))
+ log.Infof("import: skipped raw %s", clean.Log(mf.RootRelName()))
return nil
}
@@ -218,9 +220,9 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
for _, directory := range directories {
if fs.IsEmpty(directory) {
if err := os.Remove(directory); err != nil {
- log.Errorf("import: failed deleting empty folder %s (%s)", sanitize.Log(fs.RelName(directory, importPath)), err)
+ log.Errorf("import: failed deleting empty folder %s (%s)", clean.Log(fs.RelName(directory, importPath)), err)
} else {
- log.Infof("import: deleted empty folder %s", sanitize.Log(fs.RelName(directory, importPath)))
+ log.Infof("import: deleted empty folder %s", clean.Log(fs.RelName(directory, importPath)))
}
}
}
@@ -234,7 +236,7 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
}
if err := os.Remove(file); err != nil {
- log.Errorf("import: failed removing %s (%s)", sanitize.Log(fs.RelName(file, importPath)), err.Error())
+ log.Errorf("import: failed removing %s (%s)", clean.Log(fs.RelName(file, importPath)), err.Error())
}
}
}
@@ -277,7 +279,7 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
if f, err := entity.FirstFileByHash(mediaFile.Hash()); err == nil {
existingFilename := FileName(f.FileRoot, f.FileName)
if fs.FileExists(existingFilename) {
- return existingFilename, fmt.Errorf("%s is identical to %s (sha1 %s)", sanitize.Log(filepath.Base(mediaFile.FileName())), sanitize.Log(f.FileName), mediaFile.Hash())
+ return existingFilename, fmt.Errorf("%s is identical to %s (sha1 %s)", clean.Log(filepath.Base(mediaFile.FileName())), clean.Log(f.FileName), mediaFile.Hash())
} else {
return existingFilename, nil
}
@@ -293,7 +295,7 @@ func (imp *Import) DestinationFilename(mainFile *MediaFile, mediaFile *MediaFile
for fs.FileExists(result) {
if mediaFile.Hash() == fs.Hash(result) {
- return result, fmt.Errorf("%s already exists", sanitize.Log(fs.RelName(result, imp.originalsPath())))
+ return result, fmt.Errorf("%s already exists", clean.Log(fs.RelName(result, imp.originalsPath())))
}
iteration++
diff --git a/internal/photoprism/import_worker.go b/internal/photoprism/import_worker.go
index e12a643c1..bb88c4b60 100644
--- a/internal/photoprism/import_worker.go
+++ b/internal/photoprism/import_worker.go
@@ -8,8 +8,8 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/query"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
type ImportJob struct {
@@ -31,16 +31,16 @@ func ImportWorker(jobs <-chan ImportJob) {
related := job.Related
if related.Main == nil {
- log.Warnf("import: %s belongs to no supported media file", sanitize.Log(fs.RelName(job.FileName, impPath)))
+ log.Warnf("import: %s belongs to no supported media file", clean.Log(fs.RelName(job.FileName, impPath)))
continue
}
// Extract metadata to a JSON file with Exiftool.
if related.Main.NeedsExifToolJson() {
if jsonName, err := imp.convert.ToJson(related.Main); err != nil {
- log.Debugf("import: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(related.Main.BaseName()))
+ log.Debugf("import: %s in %s (extract metadata)", clean.Log(err.Error()), clean.Log(related.Main.BaseName()))
} else if err := related.Main.ReadExifToolJson(); err != nil {
- log.Errorf("import: %s in %s (read metadata)", sanitize.Log(err.Error()), sanitize.Log(related.Main.BaseName()))
+ log.Errorf("import: %s in %s (read metadata)", clean.Log(err.Error()), clean.Log(related.Main.BaseName()))
} else {
log.Debugf("import: created %s", filepath.Base(jsonName))
}
@@ -62,7 +62,7 @@ func ImportWorker(jobs <-chan ImportJob) {
if fs.PathExists(destDir) {
// Do nothing.
} else if err := os.MkdirAll(destDir, os.ModePerm); err != nil {
- log.Errorf("import: failed creating folder for %s (%s)", sanitize.Log(f.BaseName()), err.Error())
+ log.Errorf("import: failed creating folder for %s (%s)", clean.Log(f.BaseName()), err.Error())
} else {
destDirRel := fs.RelName(destDir, imp.originalsPath())
@@ -75,20 +75,20 @@ func ImportWorker(jobs <-chan ImportJob) {
if related.Main.HasSameName(f) {
destMainFileName = destFileName
- log.Infof("import: moving main %s file %s to %s", f.FileType(), sanitize.Log(relFileName), sanitize.Log(fs.RelName(destFileName, imp.originalsPath())))
+ log.Infof("import: moving main %s file %s to %s", f.FileType(), clean.Log(relFileName), clean.Log(fs.RelName(destFileName, imp.originalsPath())))
} else {
- log.Infof("import: moving related %s file %s to %s", f.FileType(), sanitize.Log(relFileName), sanitize.Log(fs.RelName(destFileName, imp.originalsPath())))
+ log.Infof("import: moving related %s file %s to %s", f.FileType(), clean.Log(relFileName), clean.Log(fs.RelName(destFileName, imp.originalsPath())))
}
if impOpt.Move {
if err := f.Move(destFileName); err != nil {
- logRelName := sanitize.Log(fs.RelName(destMainFileName, imp.originalsPath()))
+ logRelName := clean.Log(fs.RelName(destMainFileName, imp.originalsPath()))
log.Debugf("import: %s", err.Error())
log.Warnf("import: failed moving file to %s, is another import running at the same time?", logRelName)
}
} else {
if err := f.Copy(destFileName); err != nil {
- logRelName := sanitize.Log(fs.RelName(destMainFileName, imp.originalsPath()))
+ logRelName := clean.Log(fs.RelName(destMainFileName, imp.originalsPath()))
log.Debugf("import: %s", err.Error())
log.Warnf("import: failed copying file to %s, is another import running at the same time?", logRelName)
}
@@ -108,9 +108,9 @@ func ImportWorker(jobs <-chan ImportJob) {
// Remove duplicates to save storage.
if impOpt.RemoveExistingFiles {
if err := f.Remove(); err != nil {
- log.Errorf("import: failed deleting %s (%s)", sanitize.Log(f.BaseName()), err.Error())
+ log.Errorf("import: failed deleting %s (%s)", clean.Log(f.BaseName()), err.Error())
} else {
- log.Infof("import: deleted %s (already exists)", sanitize.Log(relFileName))
+ log.Infof("import: deleted %s (already exists)", clean.Log(relFileName))
}
}
}
@@ -120,14 +120,14 @@ func ImportWorker(jobs <-chan ImportJob) {
f, err := NewMediaFile(destMainFileName)
if err != nil {
- log.Errorf("import: %s in %s", err.Error(), sanitize.Log(fs.RelName(destMainFileName, imp.originalsPath())))
+ log.Errorf("import: %s in %s", err.Error(), clean.Log(fs.RelName(destMainFileName, imp.originalsPath())))
continue
}
// Extract metadata to a JSON file with Exiftool.
if f.NeedsExifToolJson() {
if jsonName, err := imp.convert.ToJson(f); err != nil {
- log.Debugf("import: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.RootRelName()))
+ log.Debugf("import: %s in %s (extract metadata)", clean.Log(err.Error()), clean.Log(f.RootRelName()))
} else {
log.Debugf("import: created %s", filepath.Base(jsonName))
}
@@ -136,10 +136,10 @@ func ImportWorker(jobs <-chan ImportJob) {
// Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpegFile, err := imp.convert.ToJpeg(f, false); err != nil {
- log.Errorf("import: %s in %s (convert to jpeg)", err.Error(), sanitize.Log(f.RootRelName()))
+ log.Errorf("import: %s in %s (convert to jpeg)", err.Error(), clean.Log(f.RootRelName()))
continue
} else {
- log.Debugf("import: created %s", sanitize.Log(jpegFile.BaseName()))
+ log.Debugf("import: created %s", clean.Log(jpegFile.BaseName()))
}
}
@@ -147,10 +147,10 @@ func ImportWorker(jobs <-chan ImportJob) {
if jpg, err := f.Jpeg(); err != nil {
log.Error(err)
} else if exceeds, actual := jpg.ExceedsResolution(o.ResolutionLimit); exceeds {
- log.Errorf("index: %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
+ log.Errorf("index: %s exceeds resolution limit (%d / %d MP)", clean.Log(f.RootRelName()), actual, o.ResolutionLimit)
continue
} else if err := jpg.CreateThumbnails(imp.thumbPath(), false); err != nil {
- log.Errorf("import: failed creating thumbnails for %s (%s)", sanitize.Log(f.RootRelName()), err.Error())
+ log.Errorf("import: failed creating thumbnails for %s (%s)", clean.Log(f.RootRelName()), err.Error())
continue
}
@@ -159,7 +159,7 @@ func ImportWorker(jobs <-chan ImportJob) {
// Skip import if the finding related files results in an error.
if err != nil {
- log.Errorf("import: %s in %s (find related files)", err.Error(), sanitize.Log(fs.RelName(destMainFileName, imp.originalsPath())))
+ log.Errorf("import: %s in %s (find related files)", err.Error(), clean.Log(fs.RelName(destMainFileName, imp.originalsPath())))
continue
}
@@ -172,10 +172,10 @@ func ImportWorker(jobs <-chan ImportJob) {
// Enforce file size and resolution limits.
if exceeds, actual := f.ExceedsFileSize(o.OriginalsLimit); exceeds {
- log.Warnf("import: %s exceeds file size limit (%d / %d MB)", sanitize.Log(f.RootRelName()), actual, o.OriginalsLimit)
+ log.Warnf("import: %s exceeds file size limit (%d / %d MB)", clean.Log(f.RootRelName()), actual, o.OriginalsLimit)
continue
} else if exceeds, actual = f.ExceedsResolution(o.ResolutionLimit); exceeds {
- log.Warnf("import: %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
+ log.Warnf("import: %s exceeds resolution limit (%d / %d MP)", clean.Log(f.RootRelName()), actual, o.ResolutionLimit)
continue
}
@@ -183,7 +183,7 @@ func ImportWorker(jobs <-chan ImportJob) {
res := ind.MediaFile(f, o, originalName, "")
// Log result.
- log.Infof("import: %s main %s file %s", res, f.FileType(), sanitize.Log(f.RootRelName()))
+ log.Infof("import: %s main %s file %s", res, f.FileType(), clean.Log(f.RootRelName()))
done[f.FileName()] = true
if !res.Success() {
@@ -198,7 +198,7 @@ func ImportWorker(jobs <-chan ImportJob) {
}
}
} else {
- log.Warnf("import: found no main file for %s, conversion to jpeg may have failed", sanitize.Log(f.RootRelName()))
+ log.Warnf("import: found no main file for %s, conversion to jpeg may have failed", clean.Log(f.RootRelName()))
}
for _, f := range related.Files {
@@ -214,15 +214,15 @@ func ImportWorker(jobs <-chan ImportJob) {
// Show warning if sidecar file exceeds size or resolution limit.
if exceeds, actual := f.ExceedsFileSize(o.OriginalsLimit); exceeds {
- log.Warnf("import: sidecar file %s exceeds size limit (%d / %d MB)", sanitize.Log(f.RootRelName()), actual, o.OriginalsLimit)
+ log.Warnf("import: sidecar file %s exceeds size limit (%d / %d MB)", clean.Log(f.RootRelName()), actual, o.OriginalsLimit)
} else if exceeds, actual = f.ExceedsResolution(o.ResolutionLimit); exceeds {
- log.Warnf("import: sidecar file %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
+ log.Warnf("import: sidecar file %s exceeds resolution limit (%d / %d MP)", clean.Log(f.RootRelName()), actual, o.ResolutionLimit)
}
// Extract metadata to a JSON file with Exiftool.
if f.NeedsExifToolJson() {
if jsonName, err := imp.convert.ToJson(f); err != nil {
- log.Debugf("import: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.RootRelName()))
+ log.Debugf("import: %s in %s (extract metadata)", clean.Log(err.Error()), clean.Log(f.RootRelName()))
} else {
log.Debugf("import: created %s", filepath.Base(jsonName))
}
@@ -237,7 +237,7 @@ func ImportWorker(jobs <-chan ImportJob) {
}
// Log result.
- log.Infof("import: %s related %s file %s", res, f.FileType(), sanitize.Log(f.RootRelName()))
+ log.Infof("import: %s related %s file %s", res, f.FileType(), clean.Log(f.RootRelName()))
}
}
diff --git a/internal/photoprism/index.go b/internal/photoprism/index.go
index 1bd4f8f47..758e1bfc6 100644
--- a/internal/photoprism/index.go
+++ b/internal/photoprism/index.go
@@ -9,6 +9,8 @@ import (
"strings"
"sync"
+ "github.com/photoprism/photoprism/pkg/media"
+
"github.com/karrick/godirwalk"
"github.com/photoprism/photoprism/internal/classify"
@@ -18,8 +20,8 @@ import (
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/nsfw"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Index represents an indexer that indexes files in the originals directory.
@@ -89,7 +91,7 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
optionsPath := filepath.Join(originalsPath, o.Path)
if !fs.PathExists(optionsPath) {
- event.Error(fmt.Sprintf("index: %s does not exist", sanitize.Log(optionsPath)))
+ event.Error(fmt.Sprintf("index: %s does not exist", clean.Log(optionsPath)))
return done
}
@@ -171,7 +173,7 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
done[fileName] = fs.Found
- if !fs.IsMedia(fileName) {
+ if !media.MainFile(fileName) {
return nil
}
@@ -185,7 +187,7 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
// Ignore RAW images?
if mf.IsRaw() && skipRaw {
- log.Infof("index: skipped raw %s", sanitize.Log(mf.RootRelName()))
+ log.Infof("index: skipped raw %s", clean.Log(mf.RootRelName()))
return nil
}
diff --git a/internal/photoprism/index_faces.go b/internal/photoprism/index_faces.go
index 60de2f423..060d1d1a7 100644
--- a/internal/photoprism/index_faces.go
+++ b/internal/photoprism/index_faces.go
@@ -7,7 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/thumb"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Faces finds faces in JPEG media files and returns them.
@@ -28,12 +28,12 @@ func (ind *Index) Faces(jpeg *MediaFile, expected int) face.Faces {
thumbName, err := jpeg.Thumbnail(Config().ThumbPath(), thumbSize)
if err != nil {
- log.Debugf("index: %s in %s (faces)", err, sanitize.Log(jpeg.BaseName()))
+ log.Debugf("index: %s in %s (faces)", err, clean.Log(jpeg.BaseName()))
return face.Faces{}
}
if thumbName == "" {
- log.Debugf("index: thumb %s not found in %s (faces)", thumbSize, sanitize.Log(jpeg.BaseName()))
+ log.Debugf("index: thumb %s not found in %s (faces)", thumbSize, clean.Log(jpeg.BaseName()))
return face.Faces{}
}
@@ -42,11 +42,11 @@ func (ind *Index) Faces(jpeg *MediaFile, expected int) face.Faces {
faces, err := ind.faceNet.Detect(thumbName, Config().FaceSize(), true, expected)
if err != nil {
- log.Debugf("%s in %s", err, sanitize.Log(jpeg.BaseName()))
+ log.Debugf("%s in %s", err, clean.Log(jpeg.BaseName()))
}
if l := len(faces); l > 0 {
- log.Infof("index: found %s in %s [%s]", english.Plural(l, "face", "faces"), sanitize.Log(jpeg.BaseName()), time.Since(start))
+ log.Infof("index: found %s in %s [%s]", english.Plural(l, "face", "faces"), clean.Log(jpeg.BaseName()), time.Since(start))
}
return faces
diff --git a/internal/photoprism/index_labels.go b/internal/photoprism/index_labels.go
index 3b0f00200..214e47845 100644
--- a/internal/photoprism/index_labels.go
+++ b/internal/photoprism/index_labels.go
@@ -6,7 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/classify"
"github.com/photoprism/photoprism/internal/thumb"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Labels classifies a JPEG image and returns matching labels.
@@ -27,14 +27,14 @@ func (ind *Index) Labels(jpeg *MediaFile) (results classify.Labels) {
filename, err := jpeg.Thumbnail(Config().ThumbPath(), size)
if err != nil {
- log.Debugf("%s in %s", err, sanitize.Log(jpeg.BaseName()))
+ log.Debugf("%s in %s", err, clean.Log(jpeg.BaseName()))
continue
}
imageLabels, err := ind.tensorFlow.File(filename)
if err != nil {
- log.Debugf("%s in %s", err, sanitize.Log(jpeg.BaseName()))
+ log.Debugf("%s in %s", err, clean.Log(jpeg.BaseName()))
continue
}
@@ -57,9 +57,9 @@ func (ind *Index) Labels(jpeg *MediaFile) (results classify.Labels) {
}
if l := len(labels); l == 1 {
- log.Infof("index: matched %d label with %s [%s]", l, sanitize.Log(jpeg.BaseName()), time.Since(start))
+ log.Infof("index: matched %d label with %s [%s]", l, clean.Log(jpeg.BaseName()), time.Since(start))
} else if l > 1 {
- log.Infof("index: matched %d labels with %s [%s]", l, sanitize.Log(jpeg.BaseName()), time.Since(start))
+ log.Infof("index: matched %d labels with %s [%s]", l, clean.Log(jpeg.BaseName()), time.Since(start))
}
return results
diff --git a/internal/photoprism/index_main.go b/internal/photoprism/index_main.go
index 008c17490..c4e8eab13 100644
--- a/internal/photoprism/index_main.go
+++ b/internal/photoprism/index_main.go
@@ -6,14 +6,14 @@ import (
"github.com/photoprism/photoprism/internal/query"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// IndexMain indexes the main file from a group of related files and returns the result.
func IndexMain(related *RelatedFiles, ind *Index, o IndexOptions) (result IndexResult) {
// Skip if main file is nil.
if related.Main == nil {
- result.Err = fmt.Errorf("index: no main file for %s", sanitize.Log(related.String()))
+ result.Err = fmt.Errorf("index: no main file for %s", clean.Log(related.String()))
result.Status = IndexFailed
return result
}
@@ -22,11 +22,11 @@ func IndexMain(related *RelatedFiles, ind *Index, o IndexOptions) (result IndexR
// Enforce file size and resolution limits.
if exceeds, actual := f.ExceedsFileSize(o.OriginalsLimit); exceeds {
- result.Err = fmt.Errorf("index: %s exceeds file size limit (%d / %d MB)", sanitize.Log(f.RootRelName()), actual, o.OriginalsLimit)
+ result.Err = fmt.Errorf("index: %s exceeds file size limit (%d / %d MB)", clean.Log(f.RootRelName()), actual, o.OriginalsLimit)
result.Status = IndexFailed
return result
} else if exceeds, actual = f.ExceedsResolution(o.ResolutionLimit); exceeds {
- result.Err = fmt.Errorf("index: %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
+ result.Err = fmt.Errorf("index: %s exceeds resolution limit (%d / %d MP)", clean.Log(f.RootRelName()), actual, o.ResolutionLimit)
result.Status = IndexFailed
return result
}
@@ -34,7 +34,7 @@ func IndexMain(related *RelatedFiles, ind *Index, o IndexOptions) (result IndexR
// Extract metadata to a JSON file with Exiftool.
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
- log.Debugf("index: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.RootRelName()))
+ log.Debugf("index: %s in %s (extract metadata)", clean.Log(err.Error()), clean.Log(f.RootRelName()))
} else {
log.Debugf("index: created %s", filepath.Base(jsonName))
}
@@ -43,18 +43,18 @@ func IndexMain(related *RelatedFiles, ind *Index, o IndexOptions) (result IndexR
// Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpg, err := ind.convert.ToJpeg(f, false); err != nil {
- result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.RootRelName()), err.Error())
+ result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", clean.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed
return result
} else if exceeds, actual := jpg.ExceedsResolution(o.ResolutionLimit); exceeds {
- result.Err = fmt.Errorf("index: %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
+ result.Err = fmt.Errorf("index: %s exceeds resolution limit (%d / %d MP)", clean.Log(f.RootRelName()), actual, o.ResolutionLimit)
result.Status = IndexFailed
return result
} else {
- log.Debugf("index: created %s", sanitize.Log(jpg.BaseName()))
+ log.Debugf("index: created %s", clean.Log(jpg.BaseName()))
if err := jpg.CreateThumbnails(ind.thumbPath(), false); err != nil {
- result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(f.RootRelName()), err.Error())
+ result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", clean.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed
return result
}
@@ -77,12 +77,12 @@ func IndexMain(related *RelatedFiles, ind *Index, o IndexOptions) (result IndexR
log.Error(result.Err)
if exists {
- log.Errorf("index: %s updating main %s file %s", result, f.FileType(), sanitize.Log(f.RootRelName()))
+ log.Errorf("index: %s updating main %s file %s", result, f.FileType(), clean.Log(f.RootRelName()))
} else {
- log.Errorf("index: %s adding main %s file %s", result, f.FileType(), sanitize.Log(f.RootRelName()))
+ log.Errorf("index: %s adding main %s file %s", result, f.FileType(), clean.Log(f.RootRelName()))
}
} else {
- log.Infof("index: %s main %s file %s", result, f.FileType(), sanitize.Log(f.RootRelName()))
+ log.Infof("index: %s main %s file %s", result, f.FileType(), clean.Log(f.RootRelName()))
}
return result
diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go
index 52672c59a..a9737ed3a 100644
--- a/internal/photoprism/index_mediafile.go
+++ b/internal/photoprism/index_mediafile.go
@@ -15,8 +15,8 @@ import (
"github.com/photoprism/photoprism/internal/meta"
"github.com/photoprism/photoprism/internal/query"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -55,7 +55,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
fileRoot, fileBase, filePath, fileName := m.PathNameInfo(stripSequence)
fullBase := m.BasePrefix(false)
- logName := sanitize.Log(fileName)
+ logName := clean.Log(fileName)
fileSize, modTime, err := m.Stat()
if err != nil {
@@ -163,7 +163,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
if o.Stack {
// Same unique ID?
if photoQuery.Error != nil && Config().Settings().StackUUID() && m.MetaData().HasDocumentID() {
- photoQuery = entity.UnscopedDb().First(&photo, "uuid <> '' AND uuid = ?", sanitize.Log(m.MetaData().DocumentID))
+ photoQuery = entity.UnscopedDb().First(&photo, "uuid <> '' AND uuid = ?", clean.Log(m.MetaData().DocumentID))
if photoQuery.Error == nil {
// Found.
@@ -201,19 +201,19 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
// Detect and report changed photo UID.
if photoExists && photoUID != "" && photoUID != file.PhotoUID {
fileChanged = true
- log.Debugf("index: %s has new photo uid %s", sanitize.Log(m.BaseName()), photoUID)
+ log.Debugf("index: %s has new photo uid %s", clean.Log(m.BaseName()), photoUID)
}
// Detect and report file changes.
if fileRenamed {
fileChanged = true
- log.Debugf("index: %s was renamed", sanitize.Log(m.BaseName()))
+ log.Debugf("index: %s was renamed", clean.Log(m.BaseName()))
} else if file.Changed(fileSize, modTime) {
fileChanged = true
- log.Debugf("index: %s was modified (new size %d, old size %d, new timestamp %d, old timestamp %d)", sanitize.Log(m.BaseName()), fileSize, file.FileSize, modTime.Unix(), file.ModTime)
+ log.Debugf("index: %s was modified (new size %d, old size %d, new timestamp %d, old timestamp %d)", clean.Log(m.BaseName()), fileSize, file.FileSize, modTime.Unix(), file.ModTime)
} else if file.Missing() {
fileChanged = true
- log.Debugf("index: %s was missing", sanitize.Log(m.BaseName()))
+ log.Debugf("index: %s was missing", clean.Log(m.BaseName()))
}
}
@@ -237,7 +237,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
// Create default thumbnails if needed.
if err := m.CreateThumbnails(ind.thumbPath(), false); err != nil {
- result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(m.RootRelName()), err.Error())
+ result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", clean.Log(m.RootRelName()), err.Error())
result.Status = IndexFailed
return result
}
@@ -253,14 +253,14 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
photo.PhotoStack = entity.IsStackable
}
- if yamlName := fs.FormatYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" {
+ if yamlName := fs.YamlFile.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" {
if err := photo.LoadFromYaml(yamlName); err != nil {
log.Errorf("index: %s in %s (restore from yaml)", err.Error(), logName)
} else if err := photo.Find(); err != nil {
- log.Infof("index: %s restored from %s", sanitize.Log(m.BaseName()), sanitize.Log(filepath.Base(yamlName)))
+ log.Infof("index: %s restored from %s", clean.Log(m.BaseName()), clean.Log(filepath.Base(yamlName)))
} else {
photoExists = true
- log.Infof("index: uid %s restored from %s", photo.PhotoUID, sanitize.Log(filepath.Base(yamlName)))
+ log.Infof("index: uid %s restored from %s", photo.PhotoUID, clean.Log(filepath.Base(yamlName)))
}
}
}
@@ -393,7 +393,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
if metaData := m.MetaData(); metaData.Error == nil {
file.FileCodec = metaData.Codec
- file.SetMetaUTC(metaData.TakenAt)
+ file.SetMediaUTC(metaData.TakenAt)
file.SetFrames(metaData.Frames)
file.SetProjection(metaData.Projection)
file.SetHDR(metaData.IsHDR())
@@ -408,7 +408,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
}
if metaData.HasInstanceID() {
- log.Infof("index: %s has instance_id %s", logName, sanitize.Log(metaData.InstanceID))
+ log.Infof("index: %s has instance_id %s", logName, clean.Log(metaData.InstanceID))
file.InstanceID = metaData.InstanceID
}
@@ -452,13 +452,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
details.SetSoftware(metaData.Software, entity.SrcMeta)
if metaData.HasDocumentID() && photo.UUID == "" {
- log.Infof("index: %s has document_id %s", logName, sanitize.Log(metaData.DocumentID))
+ log.Infof("index: %s has document_id %s", logName, clean.Log(metaData.DocumentID))
photo.UUID = metaData.DocumentID
}
if metaData.HasInstanceID() {
- log.Infof("index: %s has instance_id %s", logName, sanitize.Log(metaData.InstanceID))
+ log.Infof("index: %s has instance_id %s", logName, clean.Log(metaData.InstanceID))
file.InstanceID = metaData.InstanceID
}
@@ -475,7 +475,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
file.FileHeight = m.Height()
file.FileAspectRatio = m.AspectRatio()
file.FilePortrait = m.Portrait()
- file.SetMetaUTC(metaData.TakenAt)
+ file.SetMediaUTC(metaData.TakenAt)
file.SetDuration(metaData.Duration)
file.SetFPS(metaData.FPS)
file.SetFrames(metaData.Frames)
@@ -521,13 +521,13 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
details.SetSoftware(metaData.Software, entity.SrcMeta)
if metaData.HasDocumentID() && photo.UUID == "" {
- log.Infof("index: %s has document_id %s", logName, sanitize.Log(metaData.DocumentID))
+ log.Infof("index: %s has document_id %s", logName, clean.Log(metaData.DocumentID))
photo.UUID = metaData.DocumentID
}
if metaData.HasInstanceID() {
- log.Infof("index: %s has instance_id %s", logName, sanitize.Log(metaData.InstanceID))
+ log.Infof("index: %s has instance_id %s", logName, clean.Log(metaData.InstanceID))
file.InstanceID = metaData.InstanceID
}
@@ -544,7 +544,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
file.FileHeight = m.Height()
file.FileAspectRatio = m.AspectRatio()
file.FilePortrait = m.Portrait()
- file.SetMetaUTC(metaData.TakenAt)
+ file.SetMediaUTC(metaData.TakenAt)
file.SetDuration(metaData.Duration)
file.SetFPS(metaData.FPS)
file.SetFrames(metaData.Frames)
@@ -640,7 +640,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
details.SetSoftware(metaData.Software, entity.SrcMeta)
if metaData.HasDocumentID() && photo.UUID == "" {
- log.Debugf("index: %s has document_id %s", logName, sanitize.Log(metaData.DocumentID))
+ log.Debugf("index: %s has document_id %s", logName, clean.Log(metaData.DocumentID))
photo.UUID = metaData.DocumentID
}
@@ -676,14 +676,14 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
// Set remaining file properties.
file.FileSidecar = m.IsSidecar()
file.FileVideo = m.IsVideo()
- file.FileType = string(m.FileType())
- file.MediaType = string(m.MediaType())
+ file.FileType = m.FileType().String()
+ file.MediaType = m.Media().String()
file.FileMime = m.MimeType()
file.FileOrientation = m.Orientation()
file.ModTime = modTime.Unix()
// Detect ICC color profile for JPEGs if still unknown at this point.
- if file.FileColorProfile == "" && file.FileType == string(fs.FormatJpeg) {
+ if file.FileColorProfile == "" && fs.ImageJPEG.Equal(file.FileType) {
file.SetColorProfile(m.ColorProfile())
}
@@ -868,7 +868,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID
if err := photo.SaveAsYaml(yamlFile); err != nil {
log.Errorf("index: %s in %s (update yaml)", err.Error(), logName)
} else {
- log.Debugf("index: updated yaml file %s", sanitize.Log(filepath.Base(yamlFile)))
+ log.Debugf("index: updated yaml file %s", clean.Log(filepath.Base(yamlFile)))
}
}
diff --git a/internal/photoprism/index_nsfw.go b/internal/photoprism/index_nsfw.go
index 3e67ab8f0..099f2b68c 100644
--- a/internal/photoprism/index_nsfw.go
+++ b/internal/photoprism/index_nsfw.go
@@ -3,7 +3,7 @@ package photoprism
import (
"github.com/photoprism/photoprism/internal/nsfw"
"github.com/photoprism/photoprism/internal/thumb"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// NSFW returns true if media file might be offensive and detection is enabled.
@@ -20,7 +20,7 @@ func (ind *Index) NSFW(m *MediaFile) bool {
return false
} else {
if nsfwLabels.NSFW(nsfw.ThresholdHigh) {
- log.Warnf("index: %s might contain offensive content", sanitize.Log(m.RelName(Config().OriginalsPath())))
+ log.Warnf("index: %s might contain offensive content", clean.Log(m.RelName(Config().OriginalsPath())))
return true
}
}
diff --git a/internal/photoprism/index_related.go b/internal/photoprism/index_related.go
index 8a36d4e1f..dc77e75eb 100644
--- a/internal/photoprism/index_related.go
+++ b/internal/photoprism/index_related.go
@@ -8,14 +8,14 @@ import (
"github.com/photoprism/photoprism/internal/query"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// IndexRelated indexes a group of related files and returns the result.
func IndexRelated(related RelatedFiles, ind *Index, o IndexOptions) (result IndexResult) {
// Skip if main file is nil.
if related.Main == nil {
- result.Err = fmt.Errorf("index: no main file for %s", sanitize.Log(related.String()))
+ result.Err = fmt.Errorf("index: no main file for %s", clean.Log(related.String()))
result.Status = IndexFailed
return result
}
@@ -59,15 +59,15 @@ func IndexRelated(related RelatedFiles, ind *Index, o IndexOptions) (result Inde
// Show warning if sidecar file exceeds size or resolution limit.
if exceeds, actual := f.ExceedsFileSize(o.OriginalsLimit); exceeds {
- log.Warnf("index: sidecar file %s exceeds size limit (%d / %d MB)", sanitize.Log(f.RootRelName()), actual, o.OriginalsLimit)
+ log.Warnf("index: sidecar file %s exceeds size limit (%d / %d MB)", clean.Log(f.RootRelName()), actual, o.OriginalsLimit)
} else if exceeds, actual = f.ExceedsResolution(o.ResolutionLimit); exceeds {
- log.Warnf("index: sidecar file %s exceeds resolution limit (%d / %d MP)", sanitize.Log(f.RootRelName()), actual, o.ResolutionLimit)
+ log.Warnf("index: sidecar file %s exceeds resolution limit (%d / %d MP)", clean.Log(f.RootRelName()), actual, o.ResolutionLimit)
}
// Extract metadata to a JSON file with Exiftool.
if f.NeedsExifToolJson() {
if jsonName, err := ind.convert.ToJson(f); err != nil {
- log.Debugf("index: %s in %s (extract metadata)", sanitize.Log(err.Error()), sanitize.Log(f.RootRelName()))
+ log.Debugf("index: %s in %s (extract metadata)", clean.Log(err.Error()), clean.Log(f.RootRelName()))
} else {
log.Debugf("index: created %s", filepath.Base(jsonName))
}
@@ -76,14 +76,14 @@ func IndexRelated(related RelatedFiles, ind *Index, o IndexOptions) (result Inde
// Create JPEG sidecar for media files in other formats so that thumbnails can be created.
if o.Convert && f.IsMedia() && !f.HasJpeg() {
if jpg, err := ind.convert.ToJpeg(f, false); err != nil {
- result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", sanitize.Log(f.RootRelName()), err.Error())
+ result.Err = fmt.Errorf("index: failed converting %s to jpeg (%s)", clean.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed
return result
} else {
- log.Debugf("index: created %s", sanitize.Log(jpg.BaseName()))
+ log.Debugf("index: created %s", clean.Log(jpg.BaseName()))
if err := jpg.CreateThumbnails(ind.thumbPath(), false); err != nil {
- result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", sanitize.Log(f.RootRelName()), err.Error())
+ result.Err = fmt.Errorf("index: failed creating thumbnails for %s (%s)", clean.Log(f.RootRelName()), err.Error())
result.Status = IndexFailed
return result
}
@@ -101,7 +101,7 @@ func IndexRelated(related RelatedFiles, ind *Index, o IndexOptions) (result Inde
}
// Log index result.
- log.Infof("index: %s related %s file %s", res, f.FileType(), sanitize.Log(f.RootRelName()))
+ log.Infof("index: %s related %s file %s", res, f.FileType(), clean.Log(f.RootRelName()))
}
return result
diff --git a/internal/photoprism/index_related_test.go b/internal/photoprism/index_related_test.go
index 1bd752508..a6bf6d962 100644
--- a/internal/photoprism/index_related_test.go
+++ b/internal/photoprism/index_related_test.go
@@ -30,7 +30,7 @@ func TestIndexRelated(t *testing.T) {
t.Fatal(err)
}
- testToken := rnd.Token(8)
+ testToken := rnd.GenerateToken(8)
testPath := filepath.Join(conf.OriginalsPath(), testToken)
for _, f := range testRelated.Files {
@@ -91,7 +91,7 @@ func TestIndexRelated(t *testing.T) {
t.Fatal(err)
}
- testToken := rnd.Token(8)
+ testToken := rnd.GenerateToken(8)
testPath := filepath.Join(conf.OriginalsPath(), testToken)
for _, f := range testRelated.Files {
diff --git a/internal/photoprism/mediafile.go b/internal/photoprism/mediafile.go
index 6def86b1e..5d8e5cd40 100644
--- a/internal/photoprism/mediafile.go
+++ b/internal/photoprism/mediafile.go
@@ -18,6 +18,8 @@ import (
"sync"
"time"
+ "github.com/photoprism/photoprism/pkg/media"
+
_ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff"
_ "golang.org/x/image/webp"
@@ -32,8 +34,8 @@ import (
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/capture"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -44,7 +46,7 @@ type MediaFile struct {
statErr error
modTime time.Time
fileSize int64
- fileType fs.Format
+ fileType fs.Type
mimeType string
takenAt time.Time
takenAtSrc string
@@ -67,7 +69,7 @@ func NewMediaFile(fileName string) (*MediaFile, error) {
m := &MediaFile{
fileName: fileName,
fileRoot: entity.RootUnknown,
- fileType: fs.FormatOther,
+ fileType: fs.UnknownType,
metaData: meta.NewData(),
width: -1,
height: -1,
@@ -75,9 +77,9 @@ func NewMediaFile(fileName string) (*MediaFile, error) {
// Check if file exists and is not empty.
if size, _, err := m.Stat(); err != nil {
- return m, fmt.Errorf("%s not found", sanitize.Log(m.RootRelName()))
+ return m, fmt.Errorf("%s not found", clean.Log(m.RootRelName()))
} else if size == 0 {
- return m, fmt.Errorf("%s is empty", sanitize.Log(m.RootRelName()))
+ return m, fmt.Errorf("%s is empty", clean.Log(m.RootRelName()))
}
return m, nil
@@ -137,7 +139,7 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
m.takenAt = data.TakenAt.UTC()
m.takenAtSrc = entity.SrcMeta
- log.Infof("media: %s was taken at %s (%s)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String(), m.takenAtSrc)
+ log.Infof("media: %s was taken at %s (%s)", clean.Log(filepath.Base(m.fileName)), m.takenAt.String(), m.takenAtSrc)
return m.takenAt, m.takenAtSrc
}
@@ -146,7 +148,7 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
m.takenAt = nameTime
m.takenAtSrc = entity.SrcName
- log.Infof("media: %s was taken at %s (%s)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String(), m.takenAtSrc)
+ log.Infof("media: %s was taken at %s (%s)", clean.Log(filepath.Base(m.fileName)), m.takenAt.String(), m.takenAtSrc)
return m.takenAt, m.takenAtSrc
}
@@ -157,17 +159,17 @@ func (m *MediaFile) TakenAt() (time.Time, string) {
if err != nil {
log.Warnf("media: %s (file stat)", err.Error())
- log.Infof("media: %s was taken at %s (now)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String())
+ log.Infof("media: %s was taken at %s (now)", clean.Log(filepath.Base(m.fileName)), m.takenAt.String())
return m.takenAt, m.takenAtSrc
}
if fileInfo.HasBirthTime() {
m.takenAt = fileInfo.BirthTime().UTC()
- log.Infof("media: %s was taken at %s (file birth time)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String())
+ log.Infof("media: %s was taken at %s (file birth time)", clean.Log(filepath.Base(m.fileName)), m.takenAt.String())
} else {
m.takenAt = fileInfo.ModTime().UTC()
- log.Infof("media: %s was taken at %s (file mod time)", sanitize.Log(filepath.Base(m.fileName)), m.takenAt.String())
+ log.Infof("media: %s was taken at %s (file mod time)", clean.Log(filepath.Base(m.fileName)), m.takenAt.String())
}
return m.takenAt, m.takenAtSrc
@@ -340,7 +342,7 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
// Ignore RAW images?
if f.IsRaw() && skipRaw {
- log.Debugf("media: skipped related raw file %s", sanitize.Log(f.RootRelName()))
+ log.Debugf("media: skipped related raw file %s", clean.Log(f.RootRelName()))
continue
}
@@ -371,12 +373,12 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
t = "unknown type"
}
- return result, fmt.Errorf("no supported files found for %s (%s)", sanitize.Log(m.BaseName()), t)
+ return result, fmt.Errorf("no supported files found for %s (%s)", clean.Log(m.BaseName()), t)
}
// Add hidden JPEG if exists.
if !result.ContainsJpeg() {
- if jpegName := fs.FormatJpeg.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); jpegName != "" {
+ if jpegName := fs.ImageJPEG.FindFirst(result.Main.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); jpegName != "" {
if resultFile, err := NewMediaFile(jpegName); err == nil {
result.Files = append(result.Files, resultFile)
}
@@ -688,7 +690,7 @@ func (m *MediaFile) IsGif() bool {
// IsTiff returns true if this is a TIFF image.
func (m *MediaFile) IsTiff() bool {
- return m.HasFileType(fs.FormatTiff) && m.MimeType() == fs.MimeTypeTiff
+ return m.HasFileType(fs.ImageTIFF) && m.MimeType() == fs.MimeTypeTiff
}
// IsHEIF returns true if this is a High Efficiency Image File Format image.
@@ -708,7 +710,7 @@ func (m *MediaFile) IsWebP() bool {
// IsVideo returns true if this is a video file.
func (m *MediaFile) IsVideo() bool {
- return strings.HasPrefix(m.MimeType(), "video/") || m.MediaType() == fs.MediaVideo
+ return strings.HasPrefix(m.MimeType(), "video/") || m.Media() == media.Video
}
// IsAnimatedGif returns true if it is an animated GIF.
@@ -723,35 +725,35 @@ func (m *MediaFile) IsAnimated() bool {
// IsJson return true if this media file is a json sidecar file.
func (m *MediaFile) IsJson() bool {
- return m.HasFileType(fs.FormatJson)
+ return m.HasFileType(fs.JsonFile)
}
// FileType returns the file type (jpg, gif, tiff,...).
-func (m *MediaFile) FileType() fs.Format {
+func (m *MediaFile) FileType() fs.Type {
switch {
case m.IsJpeg():
- return fs.FormatJpeg
+ return fs.ImageJPEG
case m.IsPng():
- return fs.FormatPng
+ return fs.ImagePNG
case m.IsGif():
- return fs.FormatGif
+ return fs.ImageGIF
case m.IsHEIF():
- return fs.FormatHEIF
+ return fs.ImageHEIF
case m.IsBitmap():
- return fs.FormatBitmap
+ return fs.ImageBMP
default:
- return fs.FileFormat(m.fileName)
+ return fs.FileType(m.fileName)
}
}
-// MediaType returns the media type (video, image, raw, sidecar,...).
-func (m *MediaFile) MediaType() fs.MediaType {
- return fs.GetMediaType(m.fileName)
+// Media returns the media content type (video, image, raw, sidecar,...).
+func (m *MediaFile) Media() media.Type {
+ return media.FromName(m.fileName)
}
// HasFileType returns true if this is the given type.
-func (m *MediaFile) HasFileType(fileType fs.Format) bool {
- if fileType == fs.FormatJpeg {
+func (m *MediaFile) HasFileType(fileType fs.Type) bool {
+ if fileType == fs.ImageJPEG {
return m.IsJpeg()
}
@@ -760,12 +762,12 @@ func (m *MediaFile) HasFileType(fileType fs.Format) bool {
// IsRaw returns true if this is a RAW file.
func (m *MediaFile) IsRaw() bool {
- return m.HasFileType(fs.FormatRaw)
+ return m.HasFileType(fs.RawImage)
}
// IsXMP returns true if this is a XMP sidecar file.
func (m *MediaFile) IsXMP() bool {
- return m.FileType() == fs.FormatXMP
+ return m.FileType() == fs.XmpFile
}
// InOriginals checks if the file is stored in the 'originals' folder.
@@ -780,12 +782,12 @@ func (m *MediaFile) InSidecar() bool {
// IsSidecar checks if the file is a metadata sidecar file, independent of the storage location.
func (m *MediaFile) IsSidecar() bool {
- return m.MediaType() == fs.MediaSidecar
+ return m.Media() == media.Sidecar
}
// IsPlayableVideo checks if the file is a video in playable format.
func (m *MediaFile) IsPlayableVideo() bool {
- return m.IsVideo() && (m.HasFileType(fs.FormatMp4) || m.HasFileType(fs.FormatAVC))
+ return m.IsVideo() && (m.HasFileType(fs.VideoMP4) || m.HasFileType(fs.VideoAVC))
}
// IsImageOther returns true if this is a PNG, GIF, BMP, TIFF, or WebP file.
@@ -811,11 +813,11 @@ func (m *MediaFile) IsImage() bool {
// IsLive checks if the file is a live photo.
func (m *MediaFile) IsLive() bool {
if m.IsHEIF() {
- return fs.FormatMov.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
+ return fs.VideoMOV.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
}
if m.IsVideo() {
- return fs.FormatHEIF.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
+ return fs.ImageHEIF.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
}
return false
@@ -841,7 +843,7 @@ func (m *MediaFile) Jpeg() (*MediaFile, error) {
return m, nil
}
- jpegFilename := fs.FormatJpeg.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
+ jpegFilename := fs.ImageJPEG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
if jpegFilename == "" {
return nil, fmt.Errorf("no jpeg found for %s", m.FileName())
@@ -861,7 +863,7 @@ func (m *MediaFile) HasJpeg() bool {
return true
}
- jpegName := fs.FormatJpeg.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
+ jpegName := fs.ImageJPEG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
if jpegName == "" {
m.hasJpeg = false
@@ -874,7 +876,7 @@ func (m *MediaFile) HasJpeg() bool {
func (m *MediaFile) decodeDimensions() error {
if !m.IsMedia() {
- return fmt.Errorf("failed decoding dimensions of %s file", sanitize.Log(m.Extension()))
+ return fmt.Errorf("failed decoding dimensions of %s file", clean.Log(m.Extension()))
}
// Media dimensions already known?
@@ -918,7 +920,7 @@ func (m *MediaFile) decodeDimensions() error {
func (m *MediaFile) DecodeConfig() (_ *image.Config, err error) {
defer func() {
if r := recover(); r != nil {
- err = fmt.Errorf("panic %s while decoding %s dimensions\nstack: %s", r, sanitize.Log(m.Extension()), debug.Stack())
+ err = fmt.Errorf("panic %s while decoding %s dimensions\nstack: %s", r, clean.Log(m.Extension()), debug.Stack())
}
}()
@@ -927,7 +929,7 @@ func (m *MediaFile) DecodeConfig() (_ *image.Config, err error) {
}
if !m.IsImageNative() {
- return nil, fmt.Errorf("%s not supported natively", sanitize.Log(m.Extension()))
+ return nil, fmt.Errorf("%s not supported natively", clean.Log(m.Extension()))
}
m.fileMutex.Lock()
@@ -1066,7 +1068,7 @@ func (m *MediaFile) Thumbnail(path string, sizeName thumb.Name) (filename string
thumbnail, err := thumb.FromFile(m.FileName(), m.Hash(), path, size.Width, size.Height, m.Orientation(), size.Options...)
if err != nil {
- err = fmt.Errorf("media: failed creating thumbnail for %s (%s)", sanitize.Log(m.BaseName()), err)
+ err = fmt.Errorf("media: failed creating thumbnail for %s (%s)", clean.Log(m.BaseName()), err)
log.Debug(err)
return "", err
}
@@ -1120,7 +1122,7 @@ func (m *MediaFile) CreateThumbnails(thumbPath string, force bool) (err error) {
}
if fileName, err := thumb.FileName(hash, thumbPath, size.Width, size.Height, size.Options...); err != nil {
- log.Errorf("media: failed creating %s (%s)", sanitize.Log(string(name)), err)
+ log.Errorf("media: failed creating %s (%s)", clean.Log(string(name)), err)
return err
} else {
@@ -1132,7 +1134,7 @@ func (m *MediaFile) CreateThumbnails(thumbPath string, force bool) (err error) {
img, err := thumb.Open(m.FileName(), m.Orientation())
if err != nil {
- log.Debugf("media: %s in %s", err.Error(), sanitize.Log(m.BaseName()))
+ log.Debugf("media: %s in %s", err.Error(), clean.Log(m.BaseName()))
return err
}
@@ -1151,7 +1153,7 @@ func (m *MediaFile) CreateThumbnails(thumbPath string, force bool) (err error) {
}
if err != nil {
- log.Errorf("media: failed creating %s (%s)", sanitize.Log(string(name)), err)
+ log.Errorf("media: failed creating %s (%s)", clean.Log(string(name)), err)
return err
}
@@ -1186,9 +1188,9 @@ func (m *MediaFile) RenameSidecars(oldFileName string) (renamed map[string]strin
renamed[fs.RelName(srcName, sidecarPath)] = fs.RelName(destName, sidecarPath)
if err := os.Remove(srcName); err != nil {
- log.Errorf("media: failed removing sidecar %s", sanitize.Log(fs.RelName(srcName, sidecarPath)))
+ log.Errorf("media: failed removing sidecar %s", clean.Log(fs.RelName(srcName, sidecarPath)))
} else {
- log.Infof("media: removed sidecar %s", sanitize.Log(fs.RelName(srcName, sidecarPath)))
+ log.Infof("media: removed sidecar %s", clean.Log(fs.RelName(srcName, sidecarPath)))
}
continue
@@ -1197,7 +1199,7 @@ func (m *MediaFile) RenameSidecars(oldFileName string) (renamed map[string]strin
if err := fs.Move(srcName, destName); err != nil {
return renamed, err
} else {
- log.Infof("media: moved existing sidecar to %s", sanitize.Log(newName+filepath.Ext(srcName)))
+ log.Infof("media: moved existing sidecar to %s", clean.Log(newName+filepath.Ext(srcName)))
renamed[fs.RelName(srcName, sidecarPath)] = fs.RelName(destName, sidecarPath)
}
}
@@ -1222,9 +1224,9 @@ func (m *MediaFile) RemoveSidecars() (err error) {
for _, sidecarName := range matches {
if err = os.Remove(sidecarName); err != nil {
- log.Errorf("media: failed removing sidecar %s", sanitize.Log(fs.RelName(sidecarName, sidecarPath)))
+ log.Errorf("media: failed removing sidecar %s", clean.Log(fs.RelName(sidecarName, sidecarPath)))
} else {
- log.Infof("media: removed sidecar %s", sanitize.Log(fs.RelName(sidecarName, sidecarPath)))
+ log.Infof("media: removed sidecar %s", clean.Log(fs.RelName(sidecarName, sidecarPath)))
}
}
@@ -1238,7 +1240,7 @@ func (m *MediaFile) ColorProfile() string {
}
start := time.Now()
- logName := sanitize.Log(m.BaseName())
+ logName := clean.Log(m.BaseName())
m.fileMutex.Lock()
defer m.fileMutex.Unlock()
@@ -1265,7 +1267,7 @@ func (m *MediaFile) ColorProfile() string {
if iccProfile, err := md.ICCProfile(); err != nil || iccProfile == nil {
// Do nothing.
} else if profile, err := iccProfile.Description(); err == nil && profile != "" {
- log.Debugf("media: %s has color profile %s [%s]", logName, sanitize.Log(profile), time.Since(start))
+ log.Debugf("media: %s has color profile %s [%s]", logName, clean.Log(profile), time.Since(start))
m.colorProfile = profile
return m.colorProfile
}
diff --git a/internal/photoprism/mediafile_meta.go b/internal/photoprism/mediafile_meta.go
index 8123f3090..c2a651587 100644
--- a/internal/photoprism/mediafile_meta.go
+++ b/internal/photoprism/mediafile_meta.go
@@ -7,8 +7,8 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/meta"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// HasSidecarJson returns true if this file has or is a json sidecar file.
@@ -17,7 +17,7 @@ func (m *MediaFile) HasSidecarJson() bool {
return true
}
- return fs.FormatJson.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false) != ""
+ return fs.JsonFile.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false) != ""
}
// SidecarJsonName returns the corresponding JSON sidecar file name as used by Google Photos (and potentially other apps).
@@ -79,8 +79,8 @@ func (m *MediaFile) MetaData() (result meta.Data) {
// Parse regular JSON sidecar files ("img_1234.json")
if !m.IsSidecar() {
- if jsonFiles := fs.FormatJson.FindAll(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); len(jsonFiles) == 0 {
- log.Tracef("metadata: found no additional sidecar file for %s", sanitize.Log(filepath.Base(m.FileName())))
+ if jsonFiles := fs.JsonFile.FindAll(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false); len(jsonFiles) == 0 {
+ log.Tracef("metadata: found no additional sidecar file for %s", clean.Log(filepath.Base(m.FileName())))
} else {
for _, jsonFile := range jsonFiles {
jsonErr := m.metaData.JSON(jsonFile, m.BaseName())
@@ -102,7 +102,7 @@ func (m *MediaFile) MetaData() (result meta.Data) {
if err != nil {
m.metaData.Error = err
- log.Debugf("metadata: %s in %s", err, sanitize.Log(m.BaseName()))
+ log.Debugf("metadata: %s in %s", err, clean.Log(m.BaseName()))
}
})
diff --git a/internal/photoprism/mediafile_meta_test.go b/internal/photoprism/mediafile_meta_test.go
index 0b61a7348..3564c6590 100644
--- a/internal/photoprism/mediafile_meta_test.go
+++ b/internal/photoprism/mediafile_meta_test.go
@@ -5,6 +5,8 @@ import (
"path/filepath"
"testing"
+ "github.com/photoprism/photoprism/pkg/projection"
+
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/meta"
"github.com/stretchr/testify/assert"
@@ -230,7 +232,7 @@ func TestMediaFile_Exif_JPEG(t *testing.T) {
assert.Equal(t, "", data.CameraSerial)
assert.Equal(t, 6, data.FocalLength)
assert.Equal(t, 1, data.Orientation)
- assert.Equal(t, "equirectangular", data.Projection)
+ assert.Equal(t, projection.Equirectangular.String(), data.Projection)
})
t.Run("digikam.jpg", func(t *testing.T) {
diff --git a/internal/photoprism/mediafile_test.go b/internal/photoprism/mediafile_test.go
index ebd9c5a53..5e71af0f1 100644
--- a/internal/photoprism/mediafile_test.go
+++ b/internal/photoprism/mediafile_test.go
@@ -1006,7 +1006,7 @@ func TestMediaFile_Extension(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- assert.Equal(t, fs.JpegExt, mediaFile.Extension())
+ assert.Equal(t, fs.ExtJPEG, mediaFile.Extension())
})
}
@@ -1153,7 +1153,7 @@ func TestMediaFile_IsPng(t *testing.T) {
t.Fatal(err)
}
- assert.Equal(t, fs.FormatPng, mediaFile.FileType())
+ assert.Equal(t, fs.ImagePNG, mediaFile.FileType())
assert.Equal(t, "image/png", mediaFile.MimeType())
assert.Equal(t, true, mediaFile.IsPng())
})
@@ -1167,7 +1167,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- assert.Equal(t, fs.FormatJson, mediaFile.FileType())
+ assert.Equal(t, fs.JsonFile, mediaFile.FileType())
assert.Equal(t, "", mediaFile.MimeType())
assert.Equal(t, false, mediaFile.IsTiff())
})
@@ -1176,7 +1176,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- assert.Equal(t, fs.FormatTiff, mediaFile.FileType())
+ assert.Equal(t, fs.ImageTIFF, mediaFile.FileType())
assert.Equal(t, "image/tiff", mediaFile.MimeType())
assert.Equal(t, true, mediaFile.IsTiff())
})
@@ -1185,7 +1185,7 @@ func TestMediaFile_IsTiff(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- assert.Equal(t, fs.FormatTiff, mediaFile.FileType())
+ assert.Equal(t, fs.ImageTIFF, mediaFile.FileType())
assert.Equal(t, "image/tiff", mediaFile.MimeType())
assert.Equal(t, true, mediaFile.IsTiff())
})
@@ -1229,7 +1229,7 @@ func TestMediaFile_IsImageOther(t *testing.T) {
if err != nil {
t.Fatal(err)
}
- assert.Equal(t, fs.FormatBitmap, mediaFile.FileType())
+ assert.Equal(t, fs.ImageBMP, mediaFile.FileType())
assert.Equal(t, "image/bmp", mediaFile.MimeType())
assert.Equal(t, false, mediaFile.IsJpeg())
assert.Equal(t, false, mediaFile.IsGif())
@@ -1247,7 +1247,7 @@ func TestMediaFile_IsImageOther(t *testing.T) {
t.Fatal(err)
}
- assert.Equal(t, fs.FormatGif, mediaFile.FileType())
+ assert.Equal(t, fs.ImageGIF, mediaFile.FileType())
assert.Equal(t, "image/gif", mediaFile.MimeType())
assert.Equal(t, false, mediaFile.IsJpeg())
assert.Equal(t, true, mediaFile.IsGif())
@@ -1266,7 +1266,7 @@ func TestMediaFile_IsImageOther(t *testing.T) {
t.Fatal(err)
}
- assert.Equal(t, fs.FormatWebP, mediaFile.FileType())
+ assert.Equal(t, fs.ImageWebP, mediaFile.FileType())
assert.Equal(t, fs.MimeTypeWebP, mediaFile.MimeType())
assert.Equal(t, false, mediaFile.IsJpeg())
assert.Equal(t, false, mediaFile.IsGif())
@@ -2162,7 +2162,7 @@ func TestMediaFile_FileType(t *testing.T) {
assert.True(t, m.IsJpeg())
assert.Equal(t, "jpg", string(m.FileType()))
- assert.Equal(t, fs.FormatJpeg, m.FileType())
+ assert.Equal(t, fs.ImageJPEG, m.FileType())
assert.Equal(t, ".png", m.Extension())
}
diff --git a/internal/photoprism/moments.go b/internal/photoprism/moments.go
index eaddb8e3b..f7489059a 100644
--- a/internal/photoprism/moments.go
+++ b/internal/photoprism/moments.go
@@ -13,7 +13,7 @@ import (
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Moments represents a worker that creates albums based on popular locations, dates and labels.
@@ -95,11 +95,11 @@ func (w *Moments) Start() (err error) {
if a := entity.FindFolderAlbum(mom.Path); a != nil {
if a.DeletedAt != nil {
// Nothing to do.
- log.Tracef("moments: %s was deleted (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Tracef("moments: %s was deleted (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
} else if err := a.UpdateFolder(mom.Path, f.Serialize()); err != nil {
log.Errorf("moments: %s (update folder)", err.Error())
} else {
- log.Tracef("moments: %s already exists (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Tracef("moments: %s already exists (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
}
} else if a := entity.NewFolderAlbum(mom.Title(), mom.Path, f.Serialize()); a != nil {
a.AlbumYear = mom.FolderYear
@@ -110,7 +110,7 @@ func (w *Moments) Start() (err error) {
if err := a.Create(); err != nil {
log.Errorf("moments: %s (create folder)", err)
} else {
- log.Infof("moments: added %s (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Infof("moments: added %s (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
}
}
}
@@ -127,17 +127,17 @@ func (w *Moments) Start() (err error) {
}
if !a.Deleted() {
- log.Tracef("moments: %s already exists (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Tracef("moments: %s already exists (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
} else if err := a.Restore(); err != nil {
log.Errorf("moments: %s (restore month)", err.Error())
} else {
- log.Infof("moments: %s restored", sanitize.Log(a.AlbumTitle))
+ log.Infof("moments: %s restored", clean.Log(a.AlbumTitle))
}
} else if a := entity.NewMonthAlbum(mom.Title(), mom.Slug(), mom.Year, mom.Month); a != nil {
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err)
} else {
- log.Infof("moments: added %s (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Infof("moments: added %s (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
}
}
}
@@ -161,9 +161,9 @@ func (w *Moments) Start() (err error) {
if a.DeletedAt != nil {
// Nothing to do.
- log.Tracef("moments: %s was deleted (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Tracef("moments: %s was deleted (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
} else {
- log.Tracef("moments: %s already exists (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Tracef("moments: %s already exists (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
}
} else if a := entity.NewMomentsAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
a.AlbumYear = mom.Year
@@ -172,7 +172,7 @@ func (w *Moments) Start() (err error) {
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err)
} else {
- log.Infof("moments: added %s (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Infof("moments: added %s (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
}
}
}
@@ -195,11 +195,11 @@ func (w *Moments) Start() (err error) {
}
if !a.Deleted() {
- log.Tracef("moments: %s already exists (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Tracef("moments: %s already exists (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
} else if err := a.Restore(); err != nil {
log.Errorf("moments: %s (restore state)", err.Error())
} else {
- log.Infof("moments: %s restored", sanitize.Log(a.AlbumTitle))
+ log.Infof("moments: %s restored", clean.Log(a.AlbumTitle))
}
} else if a := entity.NewStateAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
a.AlbumLocation = mom.CountryName()
@@ -209,7 +209,7 @@ func (w *Moments) Start() (err error) {
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err)
} else {
- log.Infof("moments: added %s (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Infof("moments: added %s (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
}
}
}
@@ -233,20 +233,20 @@ func (w *Moments) Start() (err error) {
}
if a.DeletedAt != nil || f.Serialize() == a.AlbumFilter {
- log.Tracef("moments: %s already exists (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Tracef("moments: %s already exists (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
continue
}
if err := a.Update("AlbumFilter", f.Serialize()); err != nil {
log.Errorf("moments: %s", err.Error())
} else {
- log.Debugf("moments: updated %s (%s)", sanitize.Log(a.AlbumTitle), f.Serialize())
+ log.Debugf("moments: updated %s (%s)", clean.Log(a.AlbumTitle), f.Serialize())
}
} else if a := entity.NewMomentsAlbum(mom.Title(), mom.Slug(), f.Serialize()); a != nil {
if err := a.Create(); err != nil {
log.Errorf("moments: %s", err.Error())
} else {
- log.Infof("moments: added %s (%s)", sanitize.Log(a.AlbumTitle), a.AlbumFilter)
+ log.Infof("moments: added %s (%s)", clean.Log(a.AlbumTitle), a.AlbumFilter)
}
} else {
log.Errorf("moments: failed to create new moment %s (%s)", mom.Title(), f.Serialize())
diff --git a/internal/photoprism/purge.go b/internal/photoprism/purge.go
index ad4ff29ee..cae42a2bf 100644
--- a/internal/photoprism/purge.go
+++ b/internal/photoprism/purge.go
@@ -12,8 +12,8 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/query"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Purge represents a worker that removes missing files from search results.
@@ -87,20 +87,20 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot
if file.FileMissing {
if fs.FileExists(fileName) {
if opt.Dry {
- log.Infof("purge: found %s", sanitize.Log(file.FileName))
+ log.Infof("purge: found %s", clean.Log(file.FileName))
continue
}
if err := file.Found(); err != nil {
log.Errorf("purge: %s", err)
} else {
- log.Infof("purge: found %s", sanitize.Log(file.FileName))
+ log.Infof("purge: found %s", clean.Log(file.FileName))
}
}
} else if !fs.FileExists(fileName) {
if opt.Dry {
purgedFiles[fileName] = true
- log.Infof("purge: file %s would be flagged as missing", sanitize.Log(file.FileName))
+ log.Infof("purge: file %s would be flagged as missing", clean.Log(file.FileName))
continue
}
@@ -113,7 +113,7 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot
w.files.Remove(file.FileName, file.FileRoot)
purgedFiles[fileName] = true
- log.Infof("purge: flagged file %s as missing", sanitize.Log(file.FileName))
+ log.Infof("purge: flagged file %s as missing", clean.Log(file.FileName))
if !wasPrimary {
continue
@@ -162,7 +162,7 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot
if !fs.FileExists(fileName) {
if opt.Dry {
purgedFiles[fileName] = true
- log.Infof("purge: duplicate %s would be removed", sanitize.Log(file.FileName))
+ log.Infof("purge: duplicate %s would be removed", clean.Log(file.FileName))
continue
}
@@ -171,7 +171,7 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot
} else {
w.files.Remove(file.FileName, file.FileRoot)
purgedFiles[fileName] = true
- log.Infof("purge: removed duplicate %s", sanitize.Log(file.FileName))
+ log.Infof("purge: removed duplicate %s", clean.Log(file.FileName))
}
}
}
@@ -210,7 +210,7 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot
if opt.Dry {
purgedPhotos[photo.PhotoUID] = true
- log.Infof("purge: %s would be removed", sanitize.Log(photo.PhotoName))
+ log.Infof("purge: %s would be removed", clean.Log(photo.PhotoName))
continue
}
@@ -220,9 +220,9 @@ func (w *Purge) Start(opt PurgeOptions) (purgedFiles map[string]bool, purgedPhot
purgedPhotos[photo.PhotoUID] = true
if opt.Hard {
- log.Infof("purge: permanently removed %s", sanitize.Log(photo.PhotoName))
+ log.Infof("purge: permanently removed %s", clean.Log(photo.PhotoName))
} else {
- log.Infof("purge: flagged photo %s as deleted", sanitize.Log(photo.PhotoName))
+ log.Infof("purge: flagged photo %s as deleted", clean.Log(photo.PhotoName))
}
// Remove files from lookup table.
diff --git a/internal/photoprism/related.go b/internal/photoprism/related.go
index 991defa6a..b69fa6b69 100644
--- a/internal/photoprism/related.go
+++ b/internal/photoprism/related.go
@@ -3,7 +3,7 @@ package photoprism
import (
"strings"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// RelatedFiles represents a list of related files to be indexed or imported.
@@ -67,5 +67,5 @@ func (m RelatedFiles) MainLogName() string {
return ""
}
- return sanitize.Log(m.Main.RootRelName())
+ return clean.Log(m.Main.RootRelName())
}
diff --git a/internal/photoprism/related_test.go b/internal/photoprism/related_test.go
index ded649d52..8eba74609 100644
--- a/internal/photoprism/related_test.go
+++ b/internal/photoprism/related_test.go
@@ -156,7 +156,7 @@ func TestRelatedFiles_MainFileType(t *testing.T) {
Files: MediaFiles{},
Main: mediaFile,
}
- assert.Equal(t, string(fs.FormatJpeg), relatedFiles.MainFileType())
+ assert.Equal(t, string(fs.ImageJPEG), relatedFiles.MainFileType())
})
t.Run("Heif", func(t *testing.T) {
mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/telegram_2020-01-30_09-57-18.jpg")
@@ -175,7 +175,7 @@ func TestRelatedFiles_MainFileType(t *testing.T) {
Files: MediaFiles{mediaFile, mediaFile2},
Main: mediaFile3,
}
- assert.Equal(t, string(fs.FormatHEIF), relatedFiles.MainFileType())
+ assert.Equal(t, string(fs.ImageHEIF), relatedFiles.MainFileType())
})
}
diff --git a/internal/query/account_uploads.go b/internal/query/account_uploads.go
index 2344fe442..c3395ef26 100644
--- a/internal/query/account_uploads.go
+++ b/internal/query/account_uploads.go
@@ -11,7 +11,7 @@ func AccountUploads(a entity.Account, limit int) (results entity.Files, err erro
Where("files.id NOT IN (SELECT file_id FROM files_sync WHERE file_id > 0 AND account_id = ?)", a.ID)
if !a.SyncRaw {
- s = s.Where("files.file_type <> ? OR files.file_type IS NULL", fs.FormatRaw)
+ s = s.Where("files.file_type <> ? OR files.file_type IS NULL", fs.RawImage)
}
s = s.Order("files.file_name ASC")
diff --git a/internal/query/albums.go b/internal/query/albums.go
index 4a98b8413..657026e02 100644
--- a/internal/query/albums.go
+++ b/internal/query/albums.go
@@ -7,7 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/search"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Albums returns a slice of albums.
@@ -52,7 +52,7 @@ func AlbumCoverByUID(uid string) (file entity.File, err error) {
if err := a.Delete(); err != nil {
log.Errorf("%s: %s (hide)", a.AlbumType, err)
} else {
- log.Infof("%s: %s hidden", a.AlbumType, sanitize.Log(a.AlbumTitle))
+ log.Infof("%s: %s hidden", a.AlbumType, clean.Log(a.AlbumTitle))
}
}
diff --git a/internal/query/categories.go b/internal/query/categories.go
index 59e459f9b..cd0b67626 100644
--- a/internal/query/categories.go
+++ b/internal/query/categories.go
@@ -1,8 +1,6 @@
package query
-import (
- "strings"
-)
+import "github.com/photoprism/photoprism/pkg/txt"
type CategoryLabel struct {
Name string
@@ -24,7 +22,7 @@ func CategoryLabels(limit, offset int) (results []CategoryLabel) {
}
for i, l := range results {
- results[i].Title = strings.Title(l.Name)
+ results[i].Title = txt.Title(l.Name)
}
return results
diff --git a/internal/query/counts.go b/internal/query/counts.go
index 8805a26e6..42cf8cf0f 100644
--- a/internal/query/counts.go
+++ b/internal/query/counts.go
@@ -34,7 +34,7 @@ func (c *Counts) Refresh() {
Take(c)
Db().Table("photos").
- Select("SUM(photo_type = 'video' AND photo_quality >= 0 AND photo_private = 0) AS videos, SUM(photo_type IN ('image','raw','live','animated') AND photo_quality < 3 AND photo_quality >= 0 AND photo_private = 0) AS review, SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','live','animated') AND photo_private = 0 AND photo_quality >= 0) AS photos, SUM(photo_favorite = 1 AND photo_quality >= 0) AS favorites, SUM(photo_private = 1 AND photo_quality >= 0) AS private").
+ Select("SUM(photo_type = 'video' AND photo_quality > -1 AND photo_private = 0) AS videos, SUM(photo_type IN ('image','raw','live','animated') AND photo_quality < 3 AND photo_quality > -1 AND photo_private = 0) AS review, SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','live','animated') AND photo_private = 0 AND photo_quality > -1) AS photos, SUM(photo_favorite = 1 AND photo_quality > -1) AS favorites, SUM(photo_private = 1 AND photo_quality > -1) AS private").
Where("photos.id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND (file_missing = 1 OR file_error <> ''))").
Where("deleted_at IS NULL").
Take(c)
@@ -61,11 +61,11 @@ func (c *Counts) Refresh() {
Db().Table("places").
Select("SUM(photo_count > 0) AS places").
- Where("id != 'zz'").
+ Where("id <> 'zz'").
Take(c)
Db().Table("photos").
- Select("SUM(photo_type = 'video' AND photo_quality >= 0 AND photo_private = 0) AS videos, SUM(photo_type IN ('image','raw','live','animated') AND photo_quality < 3 AND photo_quality >= 0 AND photo_private = 0) AS review, SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','live','animated') AND photo_private = 0 AND photo_quality >= 0) AS photos, SUM(photo_favorite = 1 AND photo_quality >= 0) AS favorites, SUM(photo_private = 1 AND photo_quality >= 0) AS private").
+ Select("SUM(photo_type = 'video' AND photo_quality > -1 AND photo_private = 0) AS videos, SUM(photo_type IN ('image','raw','live','animated') AND photo_quality < 3 AND photo_quality > -1 AND photo_private = 0) AS review, SUM(photo_quality = -1) AS hidden, SUM(photo_type IN ('image','raw','live','animated') AND photo_private = 0 AND photo_quality > -1) AS photos, SUM(photo_favorite = 1 AND photo_quality > -1) AS favorites, SUM(photo_private = 1 AND photo_quality > -1) AS private").
Where("photos.id NOT IN (SELECT photo_id FROM files WHERE file_primary = 1 AND (file_missing = 1 OR file_error <> ''))").
Where("deleted_at IS NULL").
Take(c)
diff --git a/internal/query/faces.go b/internal/query/faces.go
index a64d2a21b..99f825e18 100644
--- a/internal/query/faces.go
+++ b/internal/query/faces.go
@@ -6,7 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/face"
"github.com/photoprism/photoprism/internal/mutex"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// IDs represents a list of identifier strings.
@@ -166,15 +166,15 @@ func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) {
for i := 1; i < len(merge); i++ {
if merge[i].SubjUID != subjUID {
return merged, fmt.Errorf("faces: cannot merge clusters with conflicting subjects %s <> %s",
- sanitize.Log(subjUID), sanitize.Log(merge[i].SubjUID))
+ clean.Log(subjUID), clean.Log(merge[i].SubjUID))
}
}
// Find or create merged face cluster.
if merged = entity.NewFace(merge[0].SubjUID, merge[0].FaceSrc, merge.Embeddings()); merged == nil {
- return merged, fmt.Errorf("faces: new cluster is nil for subject %s", sanitize.Log(subjUID))
+ return merged, fmt.Errorf("faces: new cluster is nil for subject %s", clean.Log(subjUID))
} else if merged = entity.FirstOrCreateFace(merged); merged == nil {
- return merged, fmt.Errorf("faces: failed creating new cluster for subject %s", sanitize.Log(subjUID))
+ return merged, fmt.Errorf("faces: failed creating new cluster for subject %s", clean.Log(subjUID))
} else if err := merged.MatchMarkers(append(merge.IDs(), "")); err != nil {
return merged, err
}
@@ -183,9 +183,9 @@ func MergeFaces(merge entity.Faces) (merged *entity.Face, err error) {
if removed, err := PurgeOrphanFaces(merge.IDs()); err != nil {
return merged, err
} else if removed > 0 {
- log.Debugf("faces: removed %d orphans for subject %s", removed, sanitize.Log(subjUID))
+ log.Debugf("faces: removed %d orphans for subject %s", removed, clean.Log(subjUID))
} else {
- log.Warnf("faces: failed removing merged clusters for subject %s", sanitize.Log(subjUID))
+ log.Warnf("faces: failed removing merged clusters for subject %s", clean.Log(subjUID))
}
return merged, err
diff --git a/internal/query/file_selection.go b/internal/query/file_selection.go
index cacceb430..1e1b7ea38 100644
--- a/internal/query/file_selection.go
+++ b/internal/query/file_selection.go
@@ -6,29 +6,51 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
+ "github.com/photoprism/photoprism/pkg/media"
)
+const MegaByte = 1024 * 1024
+
// FileSelection represents a selection filter to include/exclude certain files.
type FileSelection struct {
- Video bool
- Sidecar bool
- PrimaryOnly bool
- OriginalsOnly bool
- SizeLimit int
- Include []string
- Exclude []string
+ MaxSize int
+ Media []string
+ OmitMedia []string
+ Types []string
+ OmitTypes []string
+ Primary bool
+ Originals bool
+ Hidden bool
+ Private bool
+ Archived bool
}
-// FileSelectionAll returns options that include videos and sidecar files.
-func FileSelectionAll() FileSelection {
+// DownloadSelection selects files to download.
+func DownloadSelection(mediaRaw, mediaSidecar, originals bool) FileSelection {
+ omitMedia := make([]string, 0, 2)
+
+ if !mediaRaw {
+ omitMedia = append(omitMedia, media.Raw.String())
+ }
+
+ if !mediaSidecar {
+ omitMedia = append(omitMedia, media.Sidecar.String())
+ }
+
return FileSelection{
- Video: true,
- Sidecar: true,
- PrimaryOnly: false,
- OriginalsOnly: false,
- SizeLimit: 0,
- Include: []string{},
- Exclude: []string{},
+ OmitMedia: omitMedia,
+ Originals: originals,
+ Private: true,
+ Archived: true,
+ }
+}
+
+// ShareSelection selects files to share, for example for upload via WebDAV.
+func ShareSelection(primary bool) FileSelection {
+ return FileSelection{
+ Originals: !primary,
+ Primary: primary,
+ MaxSize: 1024 * MegaByte,
}
}
@@ -39,7 +61,6 @@ func SelectedFiles(f form.Selection, o FileSelection) (results entity.Files, err
}
var concat string
-
switch DbDialect() {
case MySQL:
concat = "CONCAT(a.path, '/%')"
@@ -49,6 +70,7 @@ func SelectedFiles(f form.Selection, o FileSelection) (results entity.Files, err
return results, fmt.Errorf("unknown sql dialect: %s", DbDialect())
}
+ // Search condition.
where := fmt.Sprintf(`photos.photo_uid IN (?)
OR photos.place_id IN (?)
OR photos.photo_uid IN (SELECT photo_uid FROM files WHERE file_uid IN (?))
@@ -61,42 +83,65 @@ func SelectedFiles(f form.Selection, o FileSelection) (results entity.Files, err
OR photos.id IN (SELECT pl.photo_id FROM photos_labels pl JOIN categories c ON c.label_id = pl.label_id JOIN labels lc ON lc.id = c.category_id AND lc.deleted_at IS NULL WHERE lc.label_uid IN (?))`,
concat, entity.Marker{}.TableName())
+ // Build search query.
s := UnscopedDb().Table("files").
Select("files.*").
Joins("JOIN photos ON photos.id = files.photo_id").
- Where("photos.deleted_at IS NULL").
- Where("files.file_missing = 0").
+ Where("files.file_missing = 0 AND files.file_name <> '' AND files.file_hash <> ''").
Where(where, f.Photos, f.Places, f.Files, f.Files, f.Files, f.Albums, f.Subjects, f.Labels, f.Labels).
Group("files.id")
- if o.OriginalsOnly {
- s = s.Where("file_root = '/'")
+ // File size limit?
+ if o.MaxSize > 0 {
+ s = s.Where("file_size < ?", o.MaxSize)
}
- if o.PrimaryOnly {
+ // Specific media types only?
+ if len(o.Media) > 0 {
+ s = s.Where("media_type IN (?)", o.Media)
+ }
+
+ // Exclude media types?
+ if len(o.OmitMedia) > 0 {
+ s = s.Where("media_type NOT IN (?)", o.OmitMedia)
+ }
+
+ // Specific file types only?
+ if len(o.Types) > 0 {
+ s = s.Where("file_type IN (?)", o.Types)
+ }
+
+ // Exclude file types?
+ if len(o.OmitTypes) > 0 {
+ s = s.Where("file_type NOT IN (?)", o.OmitTypes)
+ }
+
+ // Primary files only?
+ if o.Primary {
s = s.Where("file_primary = 1")
}
- if !o.Sidecar {
- s = s.Where("file_sidecar = 0")
+ // Files in originals only?
+ if o.Originals {
+ s = s.Where("file_root = '/'")
}
- if !o.Video {
- s = s.Where("file_video = 0")
+ // Exclude private?
+ if !o.Private {
+ s = s.Where("photos.photo_private <> 1")
}
- if o.SizeLimit > 0 {
- s = s.Where("file_size < ?", o.SizeLimit)
+ // Exclude hidden photos?
+ if !o.Hidden {
+ s = s.Where("photos.photo_quality > -1")
}
- if len(o.Include) > 0 {
- s = s.Where("file_type IN (?)", o.Include)
- }
-
- if len(o.Exclude) > 0 {
- s = s.Where("file_type NOT IN (?)", o.Exclude)
+ // Exclude archived photos?
+ if !o.Archived {
+ s = s.Where("photos.deleted_at IS NULL")
}
+ // Find and return.
if result := s.Scan(&results); result.Error != nil {
return results, result.Error
}
diff --git a/internal/query/files.go b/internal/query/files.go
index 4bbad168f..8e70fe954 100644
--- a/internal/query/files.go
+++ b/internal/query/files.go
@@ -78,7 +78,7 @@ func VideoByPhotoUID(photoUID string) (*entity.File, error) {
return &f, fmt.Errorf("photo uid required")
}
- err := Db().Where("photo_uid = ? AND (file_video = 1 OR file_type = ?)", photoUID, fs.FormatGif).
+ err := Db().Where("photo_uid = ? AND (file_video = 1 OR file_type = ?)", photoUID, fs.ImageGIF).
Order("file_video DESC, file_duration DESC, file_frames DESC").
Preload("Photo").First(&f).Error
return &f, err
diff --git a/internal/query/moments.go b/internal/query/moments.go
index c5cdccf56..d0e14630e 100644
--- a/internal/query/moments.go
+++ b/internal/query/moments.go
@@ -7,7 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/maps"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -105,7 +105,7 @@ func (m Moment) CountryName() string {
// Slug returns an identifier string for a moment.
func (m Moment) Slug() (s string) {
- state := sanitize.State(m.State, m.Country)
+ state := clean.State(m.State, m.Country)
if state == "" {
return m.TitleSlug()
@@ -131,7 +131,7 @@ func (m Moment) TitleSlug() string {
// Title returns an english title for the moment.
func (m Moment) Title() string {
- state := sanitize.State(m.State, m.Country)
+ state := clean.State(m.State, m.Country)
if m.Year == 0 && m.Month == 0 {
if m.Label != "" {
diff --git a/internal/query/photo.go b/internal/query/photo.go
index affd5e7bc..d8f30a5aa 100644
--- a/internal/query/photo.go
+++ b/internal/query/photo.go
@@ -77,7 +77,7 @@ func PhotosMissing(limit int, offset int) (entities entity.Photos, err error) {
err = Db().
Select("photos.*").
Where("id NOT IN (SELECT photo_id FROM files WHERE file_missing = 0 AND file_root = '/' AND deleted_at IS NULL)").
- Where("photos.photo_type <> ?", entity.TypeMeta).
+ Where("photos.photo_type <> ?", entity.MediaText).
Group("photos.id").
Limit(limit).Offset(offset).Find(&entities).Error
diff --git a/internal/query/photo_selection_test.go b/internal/query/photo_selection_test.go
index c915ace4e..24936d8f0 100644
--- a/internal/query/photo_selection_test.go
+++ b/internal/query/photo_selection_test.go
@@ -36,28 +36,55 @@ func TestPhotoSelection(t *testing.T) {
}
func TestFileSelection(t *testing.T) {
- t.Run("no items selected", func(t *testing.T) {
- f := form.Selection{
- Photos: []string{},
+ none := form.Selection{Photos: []string{}}
+
+ one := form.Selection{Photos: []string{"pt9jtdre2lvl0yh8"}}
+
+ two := form.Selection{Photos: []string{"pt9jtdre2lvl0yh7", "pt9jtdre2lvl0yh8"}}
+
+ many := form.Selection{
+ Files: []string{"ft8es39w45bnlqdw"},
+ Photos: []string{"pt9jtdre2lvl0y21", "pt9jtdre2lvl0y19", "pr2xu7myk7wrbk38", "pt9jtdre2lvl0yh7", "pt9jtdre2lvl0yh8"},
+ }
+
+ t.Run("EmptySelection", func(t *testing.T) {
+ sel := DownloadSelection(true, false, true)
+ if results, err := SelectedFiles(none, sel); err == nil {
+ t.Fatal("error expected")
+ } else {
+ assert.Empty(t, results)
}
-
- r, err := SelectedFiles(f, FileSelectionAll())
-
- assert.Equal(t, "no items selected", err.Error())
- assert.Empty(t, r)
})
- t.Run("files selected", func(t *testing.T) {
- f := form.Selection{
- Photos: []string{"pt9jtdre2lvl0yh7", "pt9jtdre2lvl0yh8"},
- }
-
- r, err := SelectedFiles(f, FileSelectionAll())
-
- if err != nil {
+ t.Run("DownloadSelectionRawSidecarPrivate", func(t *testing.T) {
+ sel := DownloadSelection(true, true, false)
+ if results, err := SelectedFiles(one, sel); err != nil {
t.Fatal(err)
+ } else {
+ assert.Len(t, results, 2)
+ }
+ })
+ t.Run("DownloadSelectionRawOriginals", func(t *testing.T) {
+ sel := DownloadSelection(true, false, true)
+ if results, err := SelectedFiles(two, sel); err != nil {
+ t.Fatal(err)
+ } else {
+ assert.Len(t, results, 2)
+ }
+ })
+ t.Run("ShareSelectionOriginals", func(t *testing.T) {
+ sel := ShareSelection(false)
+ if results, err := SelectedFiles(many, sel); err != nil {
+ t.Fatal(err)
+ } else {
+ assert.Len(t, results, 6)
+ }
+ })
+ t.Run("ShareSelectionPrimary", func(t *testing.T) {
+ sel := ShareSelection(true)
+ if results, err := SelectedFiles(many, sel); err != nil {
+ t.Fatal(err)
+ } else {
+ assert.Len(t, results, 4)
}
-
- assert.Equal(t, 3, len(r))
- assert.IsType(t, entity.Files{}, r)
})
}
diff --git a/internal/query/subjects.go b/internal/query/subjects.go
index 76c3eccb5..ba160c784 100644
--- a/internal/query/subjects.go
+++ b/internal/query/subjects.go
@@ -4,7 +4,7 @@ import (
"fmt"
"github.com/photoprism/photoprism/internal/entity"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// People returns the sorted names of the first 2000 people.
@@ -93,10 +93,10 @@ func CreateMarkerSubjects() (affected int64, err error) {
if name == m.MarkerName && subj != nil {
// Do nothing.
} else if subj = entity.NewSubject(m.MarkerName, entity.SubjPerson, entity.SrcMarker); subj == nil {
- log.Errorf("faces: invalid subject %s", sanitize.Log(m.MarkerName))
+ log.Errorf("faces: invalid subject %s", clean.Log(m.MarkerName))
continue
} else if subj = entity.FirstOrCreateSubject(subj); subj == nil {
- log.Errorf("faces: failed adding subject %s", sanitize.Log(m.MarkerName))
+ log.Errorf("faces: failed adding subject %s", clean.Log(m.MarkerName))
continue
} else {
affected++
diff --git a/internal/remote/discover.go b/internal/remote/discover.go
index 1bb12b528..85a9d833d 100644
--- a/internal/remote/discover.go
+++ b/internal/remote/discover.go
@@ -60,7 +60,7 @@ func Discover(rawUrl, user, pass string) (result Account, err error) {
serviceUrl.User = nil
if w := txt.Keywords(serviceUrl.Host); len(w) > 0 {
- result.AccName = strings.Title(w[0])
+ result.AccName = txt.Title(w[0])
} else {
result.AccName = serviceUrl.Host
}
diff --git a/internal/remote/webdav/webdav.go b/internal/remote/webdav/webdav.go
index 3d70b07db..1250c0e19 100644
--- a/internal/remote/webdav/webdav.go
+++ b/internal/remote/webdav/webdav.go
@@ -36,8 +36,8 @@ import (
"github.com/studio-b12/gowebdav"
"github.com/photoprism/photoprism/internal/event"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Global log instance.
@@ -180,14 +180,14 @@ func (c Client) fetchDirs(root string, recursive bool, start time.Time, timeout
func (c Client) Download(from, to string, force bool) (err error) {
defer func() {
if r := recover(); r != nil {
- log.Errorf("webdav: %s (panic)\nstack: %s", r, sanitize.Log(from))
- err = fmt.Errorf("webdav: unexpected error while downloading %s", sanitize.Log(from))
+ log.Errorf("webdav: %s (panic)\nstack: %s", r, clean.Log(from))
+ err = fmt.Errorf("webdav: unexpected error while downloading %s", clean.Log(from))
}
}()
// Skip if file already exists.
if _, err := os.Stat(to); err == nil && !force {
- return fmt.Errorf("webdav: download skipped, %s already exists", sanitize.Log(to))
+ return fmt.Errorf("webdav: download skipped, %s already exists", clean.Log(to))
}
dir := path.Dir(to)
@@ -196,10 +196,10 @@ func (c Client) Download(from, to string, force bool) (err error) {
if err != nil {
// Create local storage path.
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
- return fmt.Errorf("webdav: cannot create folder %s (%s)", sanitize.Log(dir), err)
+ return fmt.Errorf("webdav: cannot create folder %s (%s)", clean.Log(dir), err)
}
} else if !dirInfo.IsDir() {
- return fmt.Errorf("webdav: %s is not a folder", sanitize.Log(dir))
+ return fmt.Errorf("webdav: %s is not a folder", clean.Log(dir))
}
var bytes []byte
@@ -209,8 +209,8 @@ func (c Client) Download(from, to string, force bool) (err error) {
// Error?
if err != nil {
- log.Errorf("webdav: %s", sanitize.Log(err.Error()))
- return fmt.Errorf("webdav: failed downloading %s", sanitize.Log(from))
+ log.Errorf("webdav: %s", clean.Log(err.Error()))
+ return fmt.Errorf("webdav: failed downloading %s", clean.Log(from))
}
// Write data to file and return.
@@ -230,7 +230,7 @@ func (c Client) DownloadDir(from, to string, recursive, force bool) (errs []erro
if _, err = os.Stat(dest); err == nil {
// File already exists.
- msg := fmt.Errorf("webdav: %s already exists", sanitize.Log(dest))
+ msg := fmt.Errorf("webdav: %s already exists", clean.Log(dest))
log.Warn(msg)
errs = append(errs, msg)
continue
diff --git a/internal/remote/webdav/webdav_test.go b/internal/remote/webdav/webdav_test.go
index 4c61e60db..196cbb1f1 100644
--- a/internal/remote/webdav/webdav_test.go
+++ b/internal/remote/webdav/webdav_test.go
@@ -141,7 +141,7 @@ func TestClient_UploadAndDelete(t *testing.T) {
assert.IsType(t, Client{}, c)
- tempName := rnd.UUID() + fs.JpegExt
+ tempName := rnd.UUID() + fs.ExtJPEG
if err := c.Upload("testdata/example.jpg", tempName); err != nil {
t.Fatal(err)
diff --git a/internal/search/conditions.go b/internal/search/conditions.go
index 6853e8726..f06abd1ae 100644
--- a/internal/search/conditions.go
+++ b/internal/search/conditions.go
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/jinzhu/inflection"
@@ -12,7 +12,7 @@ import (
// Like escapes a string for use in a query.
func Like(s string) string {
- return strings.Trim(sanitize.SqlString(s), " |&*%")
+ return strings.Trim(clean.SqlString(s), " |&*%")
}
// LikeAny returns a single where condition matching the search words.
@@ -21,7 +21,7 @@ func LikeAny(col, s string, keywords, exact bool) (wheres []string) {
return wheres
}
- s = txt.StripOr(sanitize.SearchQuery(s))
+ s = txt.StripOr(clean.SearchQuery(s))
var wildcardThreshold int
diff --git a/internal/search/conditions_test.go b/internal/search/conditions_test.go
index 2c84817b3..85a4dc01d 100644
--- a/internal/search/conditions_test.go
+++ b/internal/search/conditions_test.go
@@ -7,7 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -232,7 +232,7 @@ func TestLikeAllNames(t *testing.T) {
}
})
t.Run("Plus", func(t *testing.T) {
- if w := LikeAllNames(Cols{"name"}, sanitize.SearchQuery("Paul + Paula")); len(w) == 2 {
+ if w := LikeAllNames(Cols{"name"}, clean.SearchQuery("Paul + Paula")); len(w) == 2 {
assert.Equal(t, "name LIKE '%Paul%'", w[0])
assert.Equal(t, "name LIKE '%Paula%'", w[1])
} else {
@@ -240,7 +240,7 @@ func TestLikeAllNames(t *testing.T) {
}
})
t.Run("And", func(t *testing.T) {
- if w := LikeAllNames(Cols{"name"}, sanitize.SearchQuery("P and Paula")); len(w) == 2 {
+ if w := LikeAllNames(Cols{"name"}, clean.SearchQuery("P and Paula")); len(w) == 2 {
assert.Equal(t, "name LIKE '%P%'", w[0])
assert.Equal(t, "name LIKE '%Paula%'", w[1])
} else {
@@ -248,7 +248,7 @@ func TestLikeAllNames(t *testing.T) {
}
})
t.Run("Or", func(t *testing.T) {
- if w := LikeAllNames(Cols{"name"}, sanitize.SearchQuery("Paul or Paula")); len(w) == 1 {
+ if w := LikeAllNames(Cols{"name"}, clean.SearchQuery("Paul or Paula")); len(w) == 1 {
assert.Equal(t, "name LIKE '%Paul%' OR name LIKE '%Paula%'", w[0])
} else {
t.Fatalf("unexpected result: %#v", w)
diff --git a/internal/search/labels.go b/internal/search/labels.go
index 8bc4381bc..a58a428d5 100644
--- a/internal/search/labels.go
+++ b/internal/search/labels.go
@@ -5,7 +5,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -59,7 +59,7 @@ func Labels(f form.SearchLabels) (results []Label, err error) {
likeString := "%" + f.Query + "%"
if result := Db().First(&label, "label_slug = ? OR custom_slug = ?", slugString, slugString); result.Error != nil {
- log.Infof("search: label %s not found", sanitize.Log(f.Query))
+ log.Infof("search: label %s not found", clean.Log(f.Query))
s = s.Where("labels.label_name LIKE ?", likeString)
} else {
@@ -71,7 +71,7 @@ func Labels(f form.SearchLabels) (results []Label, err error) {
labelIds = append(labelIds, category.LabelID)
}
- log.Infof("search: label %s includes %d categories", sanitize.Log(label.LabelName), len(labelIds))
+ log.Infof("search: label %s includes %d categories", clean.Log(label.LabelName), len(labelIds))
s = s.Where("labels.id IN (?)", labelIds)
}
diff --git a/internal/search/photos.go b/internal/search/photos.go
index 2fd2ff321..9d17bb66e 100644
--- a/internal/search/photos.go
+++ b/internal/search/photos.go
@@ -17,9 +17,15 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
+// PhotosColsAll contains all supported result column names.
var PhotosColsAll = SelectString(Photo{}, []string{"*"})
+
+// PhotosColsView contains the result column names necessary for the photo viewer.
var PhotosColsView = SelectString(Photo{}, SelectCols(GeoResult{}, []string{"*"}))
+// FileTypes contains a list of browser-compatible file formats returned by search queries.
+var FileTypes = []string{fs.ImageJPEG.String(), fs.ImagePNG.String(), fs.ImageGIF.String(), fs.ImageWebP.String()}
+
// Photos finds photos based on the search form provided and returns them as PhotoResults.
func Photos(f form.SearchPhotos) (results PhotoResults, count int, err error) {
return searchPhotos(f, PhotosColsAll)
@@ -76,9 +82,9 @@ func searchPhotos(f form.SearchPhotos, resultCols string) (results PhotoResults,
return PhotoResults{}, 0, fmt.Errorf("invalid sort order")
}
- // Show hidden files?
+ // Limit the result file types if hidden images/videos should not be found.
if !f.Hidden {
- s = s.Where("files.file_type IN (?) OR files.file_video = 1", []string{"jpg", "gif"})
+ s = s.Where("files.file_type IN (?) OR files.file_video = 1", FileTypes)
if f.Error {
s = s.Where("files.file_error <> ''")
@@ -92,6 +98,7 @@ func searchPhotos(f form.SearchPhotos, resultCols string) (results PhotoResults,
s = s.Where("files.file_primary = 1")
}
+ // Find only certain unique IDs?
if txt.NotEmpty(f.UID) {
s = s.Where("photos.photo_uid IN (?)", SplitOr(strings.ToLower(f.UID)))
@@ -162,6 +169,9 @@ func searchPhotos(f form.SearchPhotos, resultCols string) (results PhotoResults,
case terms["svg"]:
f.Query = strings.ReplaceAll(f.Query, "svg", "")
f.Vector = true
+ case terms["animated"]:
+ f.Query = strings.ReplaceAll(f.Query, "animated", "")
+ f.Animated = true
case terms["gifs"]:
f.Query = strings.ReplaceAll(f.Query, "gifs", "")
f.Animated = true
@@ -275,7 +285,7 @@ func searchPhotos(f form.SearchPhotos, resultCols string) (results PhotoResults,
// Filter for one or more subjects?
if txt.NotEmpty(f.Subject) {
for _, subj := range SplitAnd(strings.ToLower(f.Subject)) {
- if subjects := SplitOr(subj); rnd.ContainsUIDs(subjects, 'j') {
+ if subjects := SplitOr(subj); rnd.ValidIDs(subjects, 'j') {
s = s.Where(fmt.Sprintf("files.photo_id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
entity.Marker{}.TableName()), subjects)
} else {
@@ -515,7 +525,7 @@ func searchPhotos(f form.SearchPhotos, resultCols string) (results PhotoResults,
}
// Filter by album?
- if rnd.IsPPID(f.Album, 'a') {
+ if rnd.EntityUID(f.Album, 'a') {
if f.Filter != "" {
s = s.Where("files.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 1 AND pa.album_uid = ?)", f.Album)
} else {
diff --git a/internal/search/photos_geo.go b/internal/search/photos_geo.go
index 653a6d6ed..6bf1f910e 100644
--- a/internal/search/photos_geo.go
+++ b/internal/search/photos_geo.go
@@ -80,6 +80,9 @@ func PhotosGeo(f form.SearchPhotosGeo) (results GeoResults, err error) {
case terms["svg"]:
f.Query = strings.ReplaceAll(f.Query, "svg", "")
f.Vector = true
+ case terms["animated"]:
+ f.Query = strings.ReplaceAll(f.Query, "animated", "")
+ f.Animated = true
case terms["gifs"]:
f.Query = strings.ReplaceAll(f.Query, "gifs", "")
f.Animated = true
@@ -181,7 +184,7 @@ func PhotosGeo(f form.SearchPhotosGeo) (results GeoResults, err error) {
// Filter for one or more subjects?
if f.Subject != "" {
for _, subj := range SplitAnd(strings.ToLower(f.Subject)) {
- if subjects := SplitOr(subj); rnd.ContainsUIDs(subjects, 'j') {
+ if subjects := SplitOr(subj); rnd.ValidIDs(subjects, 'j') {
s = s.Where(fmt.Sprintf("photos.id IN (SELECT photo_id FROM files f JOIN %s m ON f.file_uid = m.file_uid AND m.marker_invalid = 0 WHERE subj_uid IN (?))",
entity.Marker{}.TableName()), subjects)
} else {
@@ -197,7 +200,7 @@ func PhotosGeo(f form.SearchPhotosGeo) (results GeoResults, err error) {
}
// Filter by album?
- if rnd.IsPPID(f.Album, 'a') {
+ if rnd.EntityUID(f.Album, 'a') {
if f.Filter != "" {
s = s.Where("photos.photo_uid NOT IN (SELECT photo_uid FROM photos_albums pa WHERE pa.hidden = 1 AND pa.album_uid = ?)", f.Album)
} else {
diff --git a/internal/search/photos_results.go b/internal/search/photos_results.go
index a30359d46..01c0bac62 100644
--- a/internal/search/photos_results.go
+++ b/internal/search/photos_results.go
@@ -2,13 +2,13 @@ package search
import (
"fmt"
- "strings"
"time"
"github.com/gosimple/slug"
"github.com/ulule/deepcopier"
"github.com/photoprism/photoprism/internal/entity"
+ "github.com/photoprism/photoprism/pkg/txt"
)
// Photo represents a photo search result.
@@ -117,7 +117,7 @@ func (photo *Photo) ShareBase(seq int) string {
var name string
if photo.PhotoTitle != "" {
- name = strings.Title(slug.MakeLang(photo.PhotoTitle, "en"))
+ name = txt.Title(slug.MakeLang(photo.PhotoTitle, "en"))
} else {
name = photo.PhotoUID
}
diff --git a/internal/search/subjects.go b/internal/search/subjects.go
index 1cf6a3482..dde8b6aef 100644
--- a/internal/search/subjects.go
+++ b/internal/search/subjects.go
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/txt"
@@ -162,5 +162,5 @@ func SubjectUIDs(s string) (result []string, names []string, remaining string) {
result = append(result, strings.Join(subj, txt.Or))
}
- return result, names, sanitize.SearchQuery(remaining)
+ return result, names, clean.SearchQuery(remaining)
}
diff --git a/internal/server/logger.go b/internal/server/logger.go
index fcb37a36e..a8c227a69 100644
--- a/internal/server/logger.go
+++ b/internal/server/logger.go
@@ -4,7 +4,7 @@ import (
"time"
"github.com/gin-gonic/gin"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Logger instances a Logger middleware for Gin.
@@ -33,7 +33,7 @@ func Logger() gin.HandlerFunc {
// Use debug level to keep production logs clean.
log.Debugf("http: %s %s (%3d) [%v]",
method,
- sanitize.Log(path),
+ clean.Log(path),
statusCode,
latency,
)
diff --git a/internal/server/security.go b/internal/server/security.go
index 70683760d..547f12163 100644
--- a/internal/server/security.go
+++ b/internal/server/security.go
@@ -6,7 +6,7 @@ import (
"strings"
"github.com/gin-gonic/gin"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
const (
@@ -82,7 +82,7 @@ func (s *security) process(w http.ResponseWriter, r *http.Request) error {
if !isGoodHost {
s.opt.BadHostHandler.ServeHTTP(w, r)
- return fmt.Errorf("http: bad host %s", sanitize.Log(r.Host))
+ return fmt.Errorf("http: bad host %s", clean.Log(r.Host))
}
}
diff --git a/internal/server/webdav.go b/internal/server/webdav.go
index 6148e08e2..4357e1db0 100644
--- a/internal/server/webdav.go
+++ b/internal/server/webdav.go
@@ -6,8 +6,8 @@ import (
"path/filepath"
"strings"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/auto"
@@ -20,11 +20,11 @@ const WebDAVImport = "/import"
// MarkUploadAsFavorite sets the favorite flag for newly uploaded files.
func MarkUploadAsFavorite(fileName string) {
- yamlName := fs.AbsPrefix(fileName, false) + fs.YamlExt
+ yamlName := fs.AbsPrefix(fileName, false) + fs.ExtYAML
// Abort if YAML file already exists to avoid overwriting metadata.
if fs.FileExists(yamlName) {
- log.Warnf("webdav: %s already exists", sanitize.Log(filepath.Base(yamlName)))
+ log.Warnf("webdav: %s already exists", clean.Log(filepath.Base(yamlName)))
return
}
@@ -41,7 +41,7 @@ func MarkUploadAsFavorite(fileName string) {
}
// Log success.
- log.Infof("webdav: marked %s as favorite", sanitize.Log(filepath.Base(fileName)))
+ log.Infof("webdav: marked %s as favorite", clean.Log(filepath.Base(fileName)))
}
// WebDAV handles any requests to /originals|import/*
@@ -66,11 +66,11 @@ func WebDAV(path string, router *gin.RouterGroup, conf *config.Config) {
if err != nil {
switch r.Method {
case MethodPut, MethodPost, MethodPatch, MethodDelete, MethodCopy, MethodMove:
- log.Errorf("webdav: %s in %s %s", sanitize.Log(err.Error()), sanitize.Log(r.Method), sanitize.Log(r.URL.String()))
+ log.Errorf("webdav: %s in %s %s", clean.Log(err.Error()), clean.Log(r.Method), clean.Log(r.URL.String()))
case MethodPropfind:
- log.Tracef("webdav: %s in %s %s", sanitize.Log(err.Error()), sanitize.Log(r.Method), sanitize.Log(r.URL.String()))
+ log.Tracef("webdav: %s in %s %s", clean.Log(err.Error()), clean.Log(r.Method), clean.Log(r.URL.String()))
default:
- log.Debugf("webdav: %s in %s %s", sanitize.Log(err.Error()), sanitize.Log(r.Method), sanitize.Log(r.URL.String()))
+ log.Debugf("webdav: %s in %s %s", clean.Log(err.Error()), clean.Log(r.Method), clean.Log(r.URL.String()))
}
} else {
@@ -85,7 +85,7 @@ func WebDAV(path string, router *gin.RouterGroup, conf *config.Config) {
switch r.Method {
case MethodPut, MethodPost, MethodPatch, MethodDelete, MethodCopy, MethodMove:
- log.Infof("webdav: %s %s", sanitize.Log(r.Method), sanitize.Log(r.URL.String()))
+ log.Infof("webdav: %s %s", clean.Log(r.Method), clean.Log(r.URL.String()))
if router.BasePath() == WebDAVOriginals {
auto.ShouldIndex()
@@ -93,7 +93,7 @@ func WebDAV(path string, router *gin.RouterGroup, conf *config.Config) {
auto.ShouldImport()
}
default:
- log.Tracef("webdav: %s %s", sanitize.Log(r.Method), sanitize.Log(r.URL.String()))
+ log.Tracef("webdav: %s %s", clean.Log(r.Method), clean.Log(r.URL.String()))
}
}
},
diff --git a/internal/thumb/create.go b/internal/thumb/create.go
index 438a05662..53e884d76 100644
--- a/internal/thumb/create.go
+++ b/internal/thumb/create.go
@@ -11,8 +11,8 @@ import (
"github.com/disintegration/imaging"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// Suffix returns the thumb cache file suffix.
@@ -35,7 +35,7 @@ func FileName(hash string, thumbPath string, width, height int, opts ...Resample
}
if len(hash) < 4 {
- return "", fmt.Errorf("resample: file hash is empty or too short (%s)", sanitize.Log(hash))
+ return "", fmt.Errorf("resample: file hash is empty or too short (%s)", clean.Log(hash))
}
if len(thumbPath) == 0 {
@@ -57,11 +57,11 @@ func FileName(hash string, thumbPath string, width, height int, opts ...Resample
// FromCache returns the thumb cache file name for an image.
func FromCache(imageFilename, hash, thumbPath string, width, height int, opts ...ResampleOption) (fileName string, err error) {
if len(hash) < 4 {
- return "", fmt.Errorf("resample: invalid file hash %s", sanitize.Log(hash))
+ return "", fmt.Errorf("resample: invalid file hash %s", clean.Log(hash))
}
if len(imageFilename) < 4 {
- return "", fmt.Errorf("resample: invalid file name %s", sanitize.Log(imageFilename))
+ return "", fmt.Errorf("resample: invalid file name %s", clean.Log(imageFilename))
}
fileName, err = FileName(hash, thumbPath, width, height, opts...)
@@ -98,7 +98,7 @@ func FromFile(imageFilename, hash, thumbPath string, width, height, orientation
img, err := Open(imageFilename, orientation)
if err != nil {
- log.Debugf("resample: %s in %s", err, sanitize.Log(filepath.Base(imageFilename)))
+ log.Debugf("resample: %s in %s", err, clean.Log(filepath.Base(imageFilename)))
return "", err
}
@@ -124,7 +124,7 @@ func Create(img image.Image, fileName string, width, height int, opts ...Resampl
var quality imaging.EncodeOption
- if filepath.Ext(fileName) == "."+string(fs.FormatPng) {
+ if filepath.Ext(fileName) == "."+string(fs.ImagePNG) {
quality = imaging.PNGCompressionLevel(png.DefaultCompression)
} else if width <= 150 && height <= 150 {
quality = JpegQualitySmall.EncodeOption()
@@ -135,7 +135,7 @@ func Create(img image.Image, fileName string, width, height int, opts ...Resampl
err = imaging.Save(result, fileName, quality)
if err != nil {
- log.Debugf("resample: failed to save %s", sanitize.Log(filepath.Base(fileName)))
+ log.Debugf("resample: failed to save %s", clean.Log(filepath.Base(fileName)))
return result, err
}
diff --git a/internal/thumb/create_test.go b/internal/thumb/create_test.go
index fe5fd7d44..bcec797e2 100644
--- a/internal/thumb/create_test.go
+++ b/internal/thumb/create_test.go
@@ -16,21 +16,21 @@ func TestResampleOptions(t *testing.T) {
assert.Equal(t, ResampleFillCenter, method)
assert.Equal(t, imaging.Lanczos.Support, filter.Support)
- assert.Equal(t, fs.FormatPng, format)
+ assert.Equal(t, fs.ImagePNG, format)
})
t.Run("ResampleNearestNeighbor, FillTopLeft", func(t *testing.T) {
method, filter, format := ResampleOptions(ResampleNearestNeighbor, ResampleFillTopLeft)
assert.Equal(t, ResampleFillTopLeft, method)
assert.Equal(t, imaging.NearestNeighbor.Support, filter.Support)
- assert.Equal(t, fs.FormatJpeg, format)
+ assert.Equal(t, fs.ImageJPEG, format)
})
t.Run("ResampleNearestNeighbor, FillBottomRight", func(t *testing.T) {
method, filter, format := ResampleOptions(ResampleNearestNeighbor, ResampleFillBottomRight)
assert.Equal(t, ResampleFillBottomRight, method)
assert.Equal(t, imaging.NearestNeighbor.Support, filter.Support)
- assert.Equal(t, fs.FormatJpeg, format)
+ assert.Equal(t, fs.ImageJPEG, format)
})
}
diff --git a/internal/thumb/jpeg.go b/internal/thumb/jpeg.go
index 51e0ee95d..bddabd194 100644
--- a/internal/thumb/jpeg.go
+++ b/internal/thumb/jpeg.go
@@ -5,14 +5,14 @@ import (
"path/filepath"
"github.com/disintegration/imaging"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
func Jpeg(srcFilename, jpgFilename string, orientation int) (img image.Image, err error) {
img, err = imaging.Open(srcFilename)
if err != nil {
- log.Errorf("resample: cannot open %s", sanitize.Log(filepath.Base(srcFilename)))
+ log.Errorf("resample: cannot open %s", clean.Log(filepath.Base(srcFilename)))
return img, err
}
@@ -23,7 +23,7 @@ func Jpeg(srcFilename, jpgFilename string, orientation int) (img image.Image, er
quality := JpegQuality.EncodeOption()
if err = imaging.Save(img, jpgFilename, quality); err != nil {
- log.Errorf("resample: failed to save %s", sanitize.Log(filepath.Base(jpgFilename)))
+ log.Errorf("resample: failed to save %s", clean.Log(filepath.Base(jpgFilename)))
return img, err
}
diff --git a/internal/thumb/jpeg_test.go b/internal/thumb/jpeg_test.go
index 28513e45c..825931887 100644
--- a/internal/thumb/jpeg_test.go
+++ b/internal/thumb/jpeg_test.go
@@ -14,7 +14,7 @@ func TestJpeg(t *testing.T) {
for _, ext := range formats {
t.Run(ext, func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -36,7 +36,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("OrientationFlipH", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -58,7 +58,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("OrientationFlipV", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -80,7 +80,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("OrientationRotate90", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -102,7 +102,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("OrientationRotate180", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -124,7 +124,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("OrientationTranspose", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -146,7 +146,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("OrientationTransverse", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -168,7 +168,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("OrientationUnspecified", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -190,7 +190,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("OrientationNormal", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
@@ -212,7 +212,7 @@ func TestJpeg(t *testing.T) {
})
t.Run("invalid orientation", func(t *testing.T) {
src := "testdata/example." + ext
- dst := "testdata/example." + ext + fs.JpegExt
+ dst := "testdata/example." + ext + fs.ExtJPEG
assert.NoFileExists(t, dst)
diff --git a/internal/thumb/names.go b/internal/thumb/names.go
index 542986210..e91dc2941 100644
--- a/internal/thumb/names.go
+++ b/internal/thumb/names.go
@@ -7,7 +7,7 @@ type Name string
// Jpeg returns the thumbnail name with a jpeg file extension suffix as string.
func (n Name) Jpeg() string {
- return string(n) + fs.JpegExt
+ return string(n) + fs.ExtJPEG
}
// String returns the thumbnail name as string.
diff --git a/internal/thumb/open.go b/internal/thumb/open.go
index 37143bb6b..8fa11cf58 100644
--- a/internal/thumb/open.go
+++ b/internal/thumb/open.go
@@ -18,7 +18,7 @@ func Open(fileName string, orientation int) (result image.Image, err error) {
}
// Open JPEG?
- if StandardRGB && fs.FileFormat(fileName) == fs.FormatJpeg {
+ if StandardRGB && fs.FileType(fileName) == fs.ImageJPEG {
return OpenJpeg(fileName, orientation)
}
diff --git a/internal/thumb/open_jpeg.go b/internal/thumb/open_jpeg.go
index e06c4f5ed..85c3301a6 100644
--- a/internal/thumb/open_jpeg.go
+++ b/internal/thumb/open_jpeg.go
@@ -9,8 +9,8 @@ import (
"github.com/disintegration/imaging"
"github.com/mandykoh/prism/meta/autometa"
+ "github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/colors"
- "github.com/photoprism/photoprism/pkg/sanitize"
)
// OpenJpeg loads a JPEG image from disk, rotates it, and converts the color profile if necessary.
@@ -19,7 +19,7 @@ func OpenJpeg(fileName string, orientation int) (result image.Image, err error)
return result, fmt.Errorf("filename missing")
}
- logName := sanitize.Log(filepath.Base(fileName))
+ logName := clean.Log(filepath.Base(fileName))
// Open file.
fileReader, err := os.Open(fileName)
@@ -52,7 +52,7 @@ func OpenJpeg(fileName string, orientation int) (result image.Image, err error)
// Do nothing.
log.Tracef("resample: %s has no color profile", logName)
} else if profile, err := iccProfile.Description(); err == nil && profile != "" {
- log.Tracef("resample: %s has color profile %s", logName, sanitize.Log(profile))
+ log.Tracef("resample: %s has color profile %s", logName, clean.Log(profile))
switch {
case colors.ProfileDisplayP3.Equal(profile):
img = colors.ToSRGB(img, colors.ProfileDisplayP3)
diff --git a/internal/thumb/resample_options.go b/internal/thumb/resample_options.go
index 1d30c7d22..0ce0f5a6a 100644
--- a/internal/thumb/resample_options.go
+++ b/internal/thumb/resample_options.go
@@ -27,15 +27,15 @@ var ResampleMethods = map[ResampleOption]string{
}
// ResampleOptions extracts filter, format, and method from resample options.
-func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.Format) {
+func ResampleOptions(opts ...ResampleOption) (method ResampleOption, filter imaging.ResampleFilter, format fs.Type) {
method = ResampleFit
filter = imaging.Lanczos
- format = fs.FormatJpeg
+ format = fs.ImageJPEG
for _, option := range opts {
switch option {
case ResamplePng:
- format = fs.FormatPng
+ format = fs.ImagePNG
case ResampleNearestNeighbor:
filter = imaging.NearestNeighbor
case ResampleDefault:
diff --git a/internal/video/codecs.go b/internal/video/codecs.go
deleted file mode 100644
index a3c2af656..000000000
--- a/internal/video/codecs.go
+++ /dev/null
@@ -1,9 +0,0 @@
-package video
-
-type Codec string
-
-const (
- CodecAVC Codec = "avc1"
- CodecHEVC Codec = "hvc1"
- CodecAV1 Codec = "av01"
-)
diff --git a/internal/video/formats.go b/internal/video/formats.go
deleted file mode 100644
index fc63f4c57..000000000
--- a/internal/video/formats.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package video
-
-import (
- "github.com/photoprism/photoprism/pkg/fs"
-)
-
-// Format represents a video format standard.
-type Format struct {
- File fs.Format
- Codec Codec
- Width int
- Height int
- Public bool
-}
-
-// FormatNames maps names to video format standards.
-type FormatNames map[string]Format
-
-var MP4 = Format{
- File: fs.FormatMp4,
- Codec: CodecAVC,
- Width: 0,
- Height: 0,
- Public: true,
-}
-
-var AVC = Format{
- File: fs.FormatAVC,
- Codec: CodecAVC,
- Width: 0,
- Height: 0,
- Public: true,
-}
-
-var AV1 = Format{
- File: fs.FormatAV1,
- Codec: CodecAV1,
- Width: 0,
- Height: 0,
- Public: false,
-}
-
-var HEVC = Format{
- File: fs.FormatHEVC,
- Codec: CodecHEVC,
- Width: 0,
- Height: 0,
- Public: false,
-}
-
-var Formats = FormatNames{
- "": AVC,
- "mp4": MP4,
- "avc": AVC,
- "av1": AV1,
- "hevc": HEVC,
-}
diff --git a/internal/workers/sync_refresh.go b/internal/workers/sync_refresh.go
index 086e54e71..8b1c25874 100644
--- a/internal/workers/sync_refresh.go
+++ b/internal/workers/sync_refresh.go
@@ -5,7 +5,7 @@ import (
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/internal/remote"
"github.com/photoprism/photoprism/internal/remote/webdav"
- "github.com/photoprism/photoprism/pkg/fs"
+ "github.com/photoprism/photoprism/pkg/media"
)
// Updates the local list of remote files so that they can be downloaded in batches
@@ -49,11 +49,11 @@ func (worker *Sync) refresh(a entity.Account) (complete bool, err error) {
f.RemoteSize = file.Size
// Select supported types for download
- mediaType := fs.GetMediaType(file.Name)
- switch mediaType {
- case fs.MediaImage, fs.MediaSidecar:
+ content := media.FromName(file.Name)
+ switch content {
+ case media.Image, media.Sidecar:
f.Status = entity.FileSyncNew
- case fs.MediaRaw, fs.MediaVideo:
+ case media.Raw, media.Video:
if a.SyncRaw {
f.Status = entity.FileSyncNew
}
@@ -66,7 +66,7 @@ func (worker *Sync) refresh(a entity.Account) (complete bool, err error) {
continue
}
- if f.Status == entity.FileSyncIgnore && a.SyncRaw && (mediaType == fs.MediaRaw || mediaType == fs.MediaVideo) {
+ if f.Status == entity.FileSyncIgnore && a.SyncRaw && (content == media.Raw || content == media.Video) {
worker.logError(f.Update("Status", entity.FileSyncNew))
}
diff --git a/internal/workers/sync_upload.go b/internal/workers/sync_upload.go
index 11eac0a6d..ab4b32e8e 100644
--- a/internal/workers/sync_upload.go
+++ b/internal/workers/sync_upload.go
@@ -11,7 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/remote/webdav"
- "github.com/photoprism/photoprism/pkg/sanitize"
+ "github.com/photoprism/photoprism/pkg/clean"
)
// Uploads local files to a remote account
@@ -55,7 +55,7 @@ func (worker *Sync) upload(a entity.Account) (complete bool, err error) {
continue // try again next time
}
- log.Infof("sync: uploaded %s to %s (%s)", sanitize.Log(file.FileName), sanitize.Log(remoteName), a.AccName)
+ log.Infof("sync: uploaded %s to %s (%s)", clean.Log(file.FileName), clean.Log(remoteName), a.AccName)
fileSync := entity.NewFileSync(a.ID, remoteName)
fileSync.Status = entity.FileSyncUploaded
diff --git a/pkg/clean/ascii.go b/pkg/clean/ascii.go
new file mode 100644
index 000000000..f0550a9da
--- /dev/null
+++ b/pkg/clean/ascii.go
@@ -0,0 +1,14 @@
+package clean
+
+// ASCII removes all non-ascii characters from a string and returns it.
+func ASCII(s string) string {
+ result := make([]rune, 0, len(s))
+
+ for _, r := range s {
+ if r <= 127 {
+ result = append(result, r)
+ }
+ }
+
+ return string(result)
+}
diff --git a/pkg/clean/clip.go b/pkg/clean/clip.go
new file mode 100644
index 000000000..659a69296
--- /dev/null
+++ b/pkg/clean/clip.go
@@ -0,0 +1,20 @@
+package clean
+
+import "strings"
+
+const (
+ ClipType = 64
+ ClipShortType = 8
+)
+
+// Clip shortens a string to the given number of characters, and removes all leading and trailing white space.
+func Clip(s string, maxLen int) string {
+ s = strings.TrimSpace(s)
+ l := len(s)
+
+ if l <= maxLen {
+ return s
+ } else {
+ return strings.TrimSpace(s[:maxLen])
+ }
+}
diff --git a/pkg/sanitize/const.go b/pkg/clean/const.go
similarity index 93%
rename from pkg/sanitize/const.go
rename to pkg/clean/const.go
index 090864cf2..35b3c4b99 100644
--- a/pkg/sanitize/const.go
+++ b/pkg/clean/const.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
const (
EnOr = "or"
diff --git a/pkg/sanitize/filename.go b/pkg/clean/filename.go
similarity index 97%
rename from pkg/sanitize/filename.go
rename to pkg/clean/filename.go
index e54754c86..e715f5b52 100644
--- a/pkg/sanitize/filename.go
+++ b/pkg/clean/filename.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"strings"
diff --git a/pkg/sanitize/filename_test.go b/pkg/clean/filename_test.go
similarity index 98%
rename from pkg/sanitize/filename_test.go
rename to pkg/clean/filename_test.go
index 0e91863ce..881654b3d 100644
--- a/pkg/sanitize/filename_test.go
+++ b/pkg/clean/filename_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/hex.go b/pkg/clean/hex.go
similarity index 95%
rename from pkg/sanitize/hex.go
rename to pkg/clean/hex.go
index f1a21d634..995e8192f 100644
--- a/pkg/sanitize/hex.go
+++ b/pkg/clean/hex.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"strings"
diff --git a/pkg/sanitize/hex_test.go b/pkg/clean/hex_test.go
similarity index 96%
rename from pkg/sanitize/hex_test.go
rename to pkg/clean/hex_test.go
index 826ae310a..313831860 100644
--- a/pkg/sanitize/hex_test.go
+++ b/pkg/clean/hex_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/id.go b/pkg/clean/id.go
similarity index 97%
rename from pkg/sanitize/id.go
rename to pkg/clean/id.go
index a64634c57..5f290d7ef 100644
--- a/pkg/sanitize/id.go
+++ b/pkg/clean/id.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"strconv"
diff --git a/pkg/sanitize/id_test.go b/pkg/clean/id_test.go
similarity index 98%
rename from pkg/sanitize/id_test.go
rename to pkg/clean/id_test.go
index 1e46963d0..00d9d1e56 100644
--- a/pkg/sanitize/id_test.go
+++ b/pkg/clean/id_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/log.go b/pkg/clean/log.go
similarity index 97%
rename from pkg/sanitize/log.go
rename to pkg/clean/log.go
index 247b9b931..94f3d7373 100644
--- a/pkg/sanitize/log.go
+++ b/pkg/clean/log.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"fmt"
diff --git a/pkg/sanitize/log_test.go b/pkg/clean/log_test.go
similarity index 98%
rename from pkg/sanitize/log_test.go
rename to pkg/clean/log_test.go
index 91f38fe72..60f8e98fd 100644
--- a/pkg/sanitize/log_test.go
+++ b/pkg/clean/log_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/name.go b/pkg/clean/name.go
similarity index 97%
rename from pkg/sanitize/name.go
rename to pkg/clean/name.go
index ea94a0d13..68964b364 100644
--- a/pkg/sanitize/name.go
+++ b/pkg/clean/name.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"strings"
diff --git a/pkg/sanitize/name_test.go b/pkg/clean/name_test.go
similarity index 98%
rename from pkg/sanitize/name_test.go
rename to pkg/clean/name_test.go
index ca1ccfee8..433db8538 100644
--- a/pkg/sanitize/name_test.go
+++ b/pkg/clean/name_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/path.go b/pkg/clean/path.go
similarity index 97%
rename from pkg/sanitize/path.go
rename to pkg/clean/path.go
index 054b7b86b..c6f0ad9e8 100644
--- a/pkg/sanitize/path.go
+++ b/pkg/clean/path.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"strings"
diff --git a/pkg/sanitize/path_test.go b/pkg/clean/path_test.go
similarity index 98%
rename from pkg/sanitize/path_test.go
rename to pkg/clean/path_test.go
index 73a338694..df74e1578 100644
--- a/pkg/sanitize/path_test.go
+++ b/pkg/clean/path_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/sanitize.go b/pkg/clean/sanitize.go
similarity index 98%
rename from pkg/sanitize/sanitize.go
rename to pkg/clean/sanitize.go
index e43894b37..2ac339601 100644
--- a/pkg/sanitize/sanitize.go
+++ b/pkg/clean/sanitize.go
@@ -24,7 +24,7 @@ Additional information can be found in our Developer Guide:
*/
-package sanitize
+package clean
import "strings"
diff --git a/pkg/sanitize/search.go b/pkg/clean/search.go
similarity index 98%
rename from pkg/sanitize/search.go
rename to pkg/clean/search.go
index 6a5eb9447..174c94f56 100644
--- a/pkg/sanitize/search.go
+++ b/pkg/clean/search.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"regexp"
diff --git a/pkg/sanitize/search_test.go b/pkg/clean/search_test.go
similarity index 98%
rename from pkg/sanitize/search_test.go
rename to pkg/clean/search_test.go
index c8e83d9ed..d3ed5a4e7 100644
--- a/pkg/sanitize/search_test.go
+++ b/pkg/clean/search_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/sql.go b/pkg/clean/sql.go
similarity index 98%
rename from pkg/sanitize/sql.go
rename to pkg/clean/sql.go
index edf84bfe0..ba41876a6 100644
--- a/pkg/sanitize/sql.go
+++ b/pkg/clean/sql.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
// SqlSpecial checks if the byte must be escaped/omitted in SQL.
func SqlSpecial(b byte) (special bool, omit bool) {
diff --git a/pkg/sanitize/sql_test.go b/pkg/clean/sql_test.go
similarity index 99%
rename from pkg/sanitize/sql_test.go
rename to pkg/clean/sql_test.go
index c85efcd73..5da499d28 100644
--- a/pkg/sanitize/sql_test.go
+++ b/pkg/clean/sql_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/state.go b/pkg/clean/state.go
similarity index 98%
rename from pkg/sanitize/state.go
rename to pkg/clean/state.go
index ad8314e60..e89607b6b 100644
--- a/pkg/sanitize/state.go
+++ b/pkg/clean/state.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"strings"
diff --git a/pkg/sanitize/state_test.go b/pkg/clean/state_test.go
similarity index 98%
rename from pkg/sanitize/state_test.go
rename to pkg/clean/state_test.go
index 15f63c2ef..a9d19ddbd 100644
--- a/pkg/sanitize/state_test.go
+++ b/pkg/clean/state_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/sanitize/token.go b/pkg/clean/token.go
similarity index 95%
rename from pkg/sanitize/token.go
rename to pkg/clean/token.go
index 457608a09..c4522436c 100644
--- a/pkg/sanitize/token.go
+++ b/pkg/clean/token.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"strings"
diff --git a/pkg/sanitize/token_test.go b/pkg/clean/token_test.go
similarity index 96%
rename from pkg/sanitize/token_test.go
rename to pkg/clean/token_test.go
index 12efd0711..387956ae9 100644
--- a/pkg/sanitize/token_test.go
+++ b/pkg/clean/token_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/clean/type.go b/pkg/clean/type.go
new file mode 100644
index 000000000..4d6a30a14
--- /dev/null
+++ b/pkg/clean/type.go
@@ -0,0 +1,34 @@
+package clean
+
+import (
+ "strings"
+)
+
+// Type omits invalid runes, ensures a maximum length of 32 characters, and returns the result.
+func Type(s string) string {
+ return Clip(ASCII(s), ClipType)
+}
+
+// TypeLower converts a type string to lowercase, omits invalid runes, and shortens it if needed.
+func TypeLower(s string) string {
+ return Type(strings.ToLower(s))
+}
+
+// ShortType omits invalid runes, ensures a maximum length of 8 characters, and returns the result.
+func ShortType(s string) string {
+ return Clip(ASCII(s), ClipShortType)
+}
+
+// ShortTypeLower converts a short type string to lowercase, omits invalid runes, and shortens it if needed.
+func ShortTypeLower(s string) string {
+ return ShortType(strings.ToLower(s))
+}
+
+// LogType returns an entity type string for logging.
+func LogType(entityType string) string {
+ if entityType == "" {
+ return ""
+ }
+
+ return entityType
+}
diff --git a/pkg/clean/type_test.go b/pkg/clean/type_test.go
new file mode 100644
index 000000000..a38d6a6e7
--- /dev/null
+++ b/pkg/clean/type_test.go
@@ -0,0 +1,65 @@
+package clean
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestToASCII(t *testing.T) {
+ result := ASCII("幸福 = Happiness.")
+ assert.Equal(t, " = Happiness.", result)
+}
+
+func TestClip(t *testing.T) {
+ t.Run("Foo", func(t *testing.T) {
+ result := Clip("Foo", 16)
+ assert.Equal(t, "Foo", result)
+ assert.Equal(t, 3, len(result))
+ })
+ t.Run("TrimFoo", func(t *testing.T) {
+ result := Clip(" Foo ", 16)
+ assert.Equal(t, "Foo", result)
+ assert.Equal(t, 3, len(result))
+ })
+ t.Run("TooLong", func(t *testing.T) {
+ result := Clip(" 幸福 Hanzi are logograms developed for the writing of Chinese! ", 16)
+ assert.Equal(t, "幸福 Hanzi are", result)
+ assert.Equal(t, 16, len(result))
+ })
+ t.Run("ToASCII", func(t *testing.T) {
+ result := Clip(ASCII(strings.ToLower(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")), ClipType)
+ assert.Equal(t, "hanzi are logograms developed for the writing of chinese! expres", result)
+ assert.Equal(t, 64, len(result))
+ })
+ t.Run("Empty", func(t *testing.T) {
+ result := Clip("", 999)
+ assert.Equal(t, "", result)
+ assert.Equal(t, 0, len(result))
+ })
+}
+
+func TestType(t *testing.T) {
+ result := Type(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")
+ assert.Equal(t, "Hanzi are logograms developed for the writing of Chinese! Expres", result)
+ assert.Equal(t, ClipType, len(result))
+}
+
+func TestTypeLower(t *testing.T) {
+ result := TypeLower(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")
+ assert.Equal(t, "hanzi are logograms developed for the writing of chinese! expres", result)
+ assert.Equal(t, ClipType, len(result))
+}
+
+func TestShortType(t *testing.T) {
+ result := ShortType(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")
+ assert.Equal(t, "Hanzi ar", result)
+ assert.Equal(t, ClipShortType, len(result))
+}
+
+func TestShortTypeLower(t *testing.T) {
+ result := ShortTypeLower(" 幸福 Hanzi are logograms developed for the writing of Chinese! Expressions in an index may not ...!")
+ assert.Equal(t, "hanzi ar", result)
+ assert.Equal(t, ClipShortType, len(result))
+}
diff --git a/pkg/sanitize/username.go b/pkg/clean/username.go
similarity index 94%
rename from pkg/sanitize/username.go
rename to pkg/clean/username.go
index 4e255da10..ed1fceb73 100644
--- a/pkg/sanitize/username.go
+++ b/pkg/clean/username.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"strings"
diff --git a/pkg/sanitize/username_test.go b/pkg/clean/username_test.go
similarity index 95%
rename from pkg/sanitize/username_test.go
rename to pkg/clean/username_test.go
index e02b33b17..e8b7571e0 100644
--- a/pkg/sanitize/username_test.go
+++ b/pkg/clean/username_test.go
@@ -1,4 +1,4 @@
-package sanitize
+package clean
import (
"testing"
diff --git a/pkg/colors/colorful.go b/pkg/colors/colorful.go
index bad91e04d..76082cc06 100644
--- a/pkg/colors/colorful.go
+++ b/pkg/colors/colorful.go
@@ -2,6 +2,7 @@ package colors
import "github.com/lucasb-eyer/go-colorful"
+// Colorful finds the Color most similar to the specified colorful.Color.
func Colorful(actualColor colorful.Color) (result Color) {
var distance = 1.0
diff --git a/pkg/colors/colors.go b/pkg/colors/colors.go
index 6c7821a52..be5ce0017 100644
--- a/pkg/colors/colors.go
+++ b/pkg/colors/colors.go
@@ -29,7 +29,8 @@ package colors
import (
"fmt"
"image/color"
- "strings"
+
+ "github.com/photoprism/photoprism/pkg/txt"
)
type Color uint8
@@ -137,7 +138,7 @@ func (c Colors) List() []map[string]string {
result := make([]map[string]string, 0, len(c))
for _, c := range c {
- result = append(result, map[string]string{"Slug": c.Name(), "Name": strings.Title(c.Name()), "Example": ColorExamples[c]})
+ result = append(result, map[string]string{"Slug": c.Name(), "Name": txt.UpperFirst(c.Name()), "Example": ColorExamples[c]})
}
return result
diff --git a/pkg/env/env.go b/pkg/env/env.go
new file mode 100644
index 000000000..382a99edb
--- /dev/null
+++ b/pkg/env/env.go
@@ -0,0 +1,27 @@
+/*
+
+Package env provides runtime environment information.
+
+Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
+
+ This program is free software: you can redistribute it and/or modify
+ it under Version 3 of the GNU Affero General Public License (the "AGPL"):
+
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ The AGPL is supplemented by our Trademark and Brand Guidelines,
+ which describe how our Brand Assets may be used:
+
+
+Feel free to send an email to hello@photoprism.app if you have questions,
+want to support our work, or just want to say hello.
+
+Additional information can be found in our Developer Guide:
+
+
+*/
+package env
diff --git a/pkg/env/info.go b/pkg/env/info.go
new file mode 100644
index 000000000..f79b8bbaf
--- /dev/null
+++ b/pkg/env/info.go
@@ -0,0 +1,9 @@
+package env
+
+// Info returns a new Resources instance.
+func Info() (resources Resources) {
+ resources = Resources{}
+ resources.Update()
+
+ return resources
+}
diff --git a/internal/config/runtime_test.go b/pkg/env/info_test.go
similarity index 86%
rename from internal/config/runtime_test.go
rename to pkg/env/info_test.go
index 9a7b19553..6a63f2de9 100644
--- a/internal/config/runtime_test.go
+++ b/pkg/env/info_test.go
@@ -1,4 +1,4 @@
-package config
+package env
import (
"testing"
@@ -6,8 +6,8 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestNewRuntimeInfo(t *testing.T) {
- info := NewRuntimeInfo()
+func TestNew(t *testing.T) {
+ info := Info()
assert.LessOrEqual(t, 1, info.Cores)
assert.LessOrEqual(t, 1, info.Routines)
@@ -19,8 +19,8 @@ func TestNewRuntimeInfo(t *testing.T) {
// t.Logf("Free: %s, Total: %s", humanize.Bytes(info.Memory.Free), humanize.Bytes(info.Memory.Total))
}
-func TestRuntimeInfo_Refresh(t *testing.T) {
- info := NewRuntimeInfo()
+func TestInfo_Update(t *testing.T) {
+ info := Info()
assert.LessOrEqual(t, 1, info.Cores)
assert.LessOrEqual(t, 1, info.Routines)
@@ -29,7 +29,7 @@ func TestRuntimeInfo_Refresh(t *testing.T) {
assert.LessOrEqual(t, uint64(1), info.Memory.Reserved)
assert.LessOrEqual(t, uint64(1), info.Memory.Used)
- info.Refresh()
+ info.Update()
assert.LessOrEqual(t, 1, info.Cores)
assert.LessOrEqual(t, 1, info.Routines)
diff --git a/internal/config/runtime.go b/pkg/env/resources.go
similarity index 67%
rename from internal/config/runtime.go
rename to pkg/env/resources.go
index 5f799bb52..e30a4de8a 100644
--- a/internal/config/runtime.go
+++ b/pkg/env/resources.go
@@ -1,4 +1,4 @@
-package config
+package env
import (
"fmt"
@@ -8,8 +8,8 @@ import (
"github.com/pbnjay/memory"
)
-// RuntimeInfo represents memory and cpu usage statistics.
-type RuntimeInfo struct {
+// Resources represents runtime resource information.
+type Resources struct {
Cores int `json:"cores"`
Routines int `json:"routines"`
Memory struct {
@@ -21,16 +21,8 @@ type RuntimeInfo struct {
} `json:"memory"`
}
-// NewRuntimeInfo returns a new RuntimeInfo instance.
-func NewRuntimeInfo() (r RuntimeInfo) {
- r = RuntimeInfo{}
- r.Refresh()
-
- return r
-}
-
-// Refresh updates runtime info options like number of goroutines and memory usage.
-func (r *RuntimeInfo) Refresh() {
+// Update runtime resource information.
+func (r *Resources) Update() {
var mem runtime.MemStats
runtime.ReadMemStats(&mem)
diff --git a/pkg/fs/case.go b/pkg/fs/case.go
index 00932011c..fad2cefc0 100644
--- a/pkg/fs/case.go
+++ b/pkg/fs/case.go
@@ -24,5 +24,5 @@ func CaseInsensitive(storagePath string) (result bool, err error) {
// IgnoreCase enables the case-insensitive mode.
func IgnoreCase() {
ignoreCase = true
- Formats = Extensions.Formats(true)
+ FileTypes = Extensions.Types(true)
}
diff --git a/pkg/fs/ext.go b/pkg/fs/ext.go
deleted file mode 100644
index be1be3efa..000000000
--- a/pkg/fs/ext.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package fs
-
-import (
- "strings"
-)
-
-const (
- YamlExt = ".yml"
- JpegExt = ".jpg"
- AvcExt = ".avc"
- FujiRawExt = ".raf"
- CanonCr3Ext = ".cr3"
-)
-
-// NormalizeExt returns the file extension without dot and in lowercase.
-func NormalizeExt(fileName string) string {
- if dot := strings.LastIndex(fileName, "."); dot != -1 && len(fileName[dot+1:]) >= 1 {
- return strings.ToLower(fileName[dot+1:])
- }
-
- return ""
-}
-
-// TrimExt removes unwanted characters from file extension strings, and makes it lowercase for comparison.
-func TrimExt(ext string) string {
- return strings.ToLower(strings.Trim(ext, " .,;:“”'`\""))
-}
diff --git a/pkg/fs/ext_test.go b/pkg/fs/ext_test.go
deleted file mode 100644
index 61ce211f3..000000000
--- a/pkg/fs/ext_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-package fs
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestNormalizeExt(t *testing.T) {
- t.Run("none", func(t *testing.T) {
- result := NormalizeExt("testdata/test")
- assert.Equal(t, "", result)
- })
-
- t.Run("dot", func(t *testing.T) {
- result := NormalizeExt("testdata/test.")
- assert.Equal(t, "", result)
- })
-
- t.Run("test.z", func(t *testing.T) {
- result := NormalizeExt("testdata/test.z")
- assert.Equal(t, "z", result)
- })
-
- t.Run("test.jpg", func(t *testing.T) {
- result := NormalizeExt("testdata/test.jpg")
- assert.Equal(t, "jpg", result)
- })
-
- t.Run("test.PNG", func(t *testing.T) {
- result := NormalizeExt("testdata/test.PNG")
- assert.Equal(t, "png", result)
- })
-
- t.Run("test.MOV", func(t *testing.T) {
- result := NormalizeExt("testdata/test.MOV")
- assert.Equal(t, "mov", result)
- })
-
- t.Run("test.xmp", func(t *testing.T) {
- result := NormalizeExt("testdata/test.xMp")
- assert.Equal(t, "xmp", result)
- })
-
- t.Run("test.MP", func(t *testing.T) {
- result := NormalizeExt("testdata/test.mp")
- assert.Equal(t, "mp", result)
- })
-}
-
-func TestTrimExt(t *testing.T) {
- t.Run("WithDot", func(t *testing.T) {
- assert.Equal(t, "raf", TrimExt(".raf"))
- })
- t.Run("Normalized", func(t *testing.T) {
- assert.Equal(t, "cr3", TrimExt("cr3"))
- })
- t.Run("Uppercase", func(t *testing.T) {
- assert.Equal(t, "aaf", TrimExt("AAF"))
- })
- t.Run("Empty", func(t *testing.T) {
- assert.Equal(t, "", TrimExt(""))
- })
- t.Run("MixedCaseWithDot", func(t *testing.T) {
- assert.Equal(t, "raw", TrimExt(".Raw"))
- })
- t.Run("TypographicQuotes", func(t *testing.T) {
- assert.Equal(t, "jpeg", TrimExt(" “JPEG” "))
- })
-}
diff --git a/pkg/fs/extension.go b/pkg/fs/extension.go
deleted file mode 100644
index c19baf2f7..000000000
--- a/pkg/fs/extension.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package fs
-
-import (
- "path/filepath"
- "strings"
-)
-
-// Ext returns all extension of a file name including the dots.
-func Ext(name string) string {
- ext := filepath.Ext(name)
- name = StripExt(name)
-
- if Extensions.Known(name) {
- ext = filepath.Ext(name) + ext
- }
-
- return ext
-}
-
-// StripExt removes the file type extension from a file name (if any).
-func StripExt(name string) string {
- if end := strings.LastIndex(name, "."); end != -1 {
- name = name[:end]
- }
-
- return name
-}
-
-// StripKnownExt removes all known file type extension from a file name (if any).
-func StripKnownExt(name string) string {
- for Extensions.Known(name) {
- name = StripExt(name)
- }
-
- return name
-}
diff --git a/pkg/fs/extensions.go b/pkg/fs/extensions.go
deleted file mode 100644
index 311423b14..000000000
--- a/pkg/fs/extensions.go
+++ /dev/null
@@ -1,152 +0,0 @@
-package fs
-
-import (
- "path/filepath"
- "strings"
-)
-
-// FileExtensions maps file extensions to standard formats
-type FileExtensions map[string]Format
-
-// Extensions contains the filename extensions of file formats known to PhotoPrism.
-var Extensions = FileExtensions{
- ".jpg": FormatJpeg,
- ".jpeg": FormatJpeg,
- ".jpe": FormatJpeg,
- ".jif": FormatJpeg,
- ".jfif": FormatJpeg,
- ".jfi": FormatJpeg,
- ".thm": FormatJpeg,
- ".3fr": FormatRaw,
- ".ari": FormatRaw,
- ".arw": FormatRaw,
- ".bay": FormatRaw,
- ".cap": FormatRaw,
- ".crw": FormatRaw,
- ".cr2": FormatRaw,
- ".cr3": FormatRaw,
- ".data": FormatRaw,
- ".dcs": FormatRaw,
- ".dcr": FormatRaw,
- ".dng": FormatRaw,
- ".drf": FormatRaw,
- ".eip": FormatRaw,
- ".erf": FormatRaw,
- ".fff": FormatRaw,
- ".gpr": FormatRaw,
- ".iiq": FormatRaw,
- ".k25": FormatRaw,
- ".kdc": FormatRaw,
- ".mdc": FormatRaw,
- ".mef": FormatRaw,
- ".mos": FormatRaw,
- ".mrw": FormatRaw,
- ".nef": FormatRaw,
- ".nrw": FormatRaw,
- ".obm": FormatRaw,
- ".orf": FormatRaw,
- ".pef": FormatRaw,
- ".ptx": FormatRaw,
- ".pxn": FormatRaw,
- ".r3d": FormatRaw,
- ".raf": FormatRaw,
- ".raw": FormatRaw,
- ".rwl": FormatRaw,
- ".rwz": FormatRaw,
- ".rw2": FormatRaw,
- ".srf": FormatRaw,
- ".srw": FormatRaw,
- ".sr2": FormatRaw,
- ".x3f": FormatRaw,
- ".png": FormatPng,
- ".pn": FormatPng,
- ".tif": FormatTiff,
- ".tiff": FormatTiff,
- ".gif": FormatGif,
- ".bmp": FormatBitmap,
- ".heif": FormatHEIF,
- ".heic": FormatHEIF,
- ".hevc": FormatHEVC,
- ".mov": FormatMov,
- ".qt": FormatMov,
- ".avi": FormatAvi,
- ".av1": FormatAV1,
- ".avc": FormatAVC,
- ".mpg": FormatMPEG,
- ".mpeg": FormatMPEG,
- ".mjpg": FormatMJPEG,
- ".mjpeg": FormatMJPEG,
- ".mp2": FormatMp2,
- ".mpv": FormatMp2,
- ".mp": FormatMp4,
- ".mp4": FormatMp4,
- ".m4v": FormatMp4,
- ".3gp": Format3gp,
- ".3g2": Format3g2,
- ".flv": FormatFlv,
- ".f4v": FormatFlv,
- ".mkv": FormatMkv,
- ".mpo": FormatMpo,
- ".mts": FormatMts,
- ".ogv": FormatOgv,
- ".ogg": FormatOgv,
- ".ogx": FormatOgv,
- ".webp": FormatWebP,
- ".webm": FormatWebM,
- ".wmv": FormatWMV,
- ".aae": FormatAAE,
- ".md": FormatMarkdown,
- ".markdown": FormatMarkdown,
- ".json": FormatJson,
- ".toml": FormatToml,
- ".txt": FormatText,
- ".yml": FormatYaml,
- ".yaml": FormatYaml,
- ".xmp": FormatXMP,
- ".xml": FormatXML,
-}
-
-// Known tests if the file extension is known (supported).
-func (m FileExtensions) Known(name string) bool {
- if name == "" {
- return false
- }
-
- ext := strings.ToLower(filepath.Ext(name))
-
- if ext == "" {
- return false
- }
-
- if _, ok := m[ext]; ok {
- return true
- }
-
- return false
-}
-
-// Formats returns all known file extensions by format.
-func (m FileExtensions) Formats(noUppercase bool) FileFormats {
- result := make(FileFormats)
-
- if noUppercase {
- for ext, t := range m {
- if _, ok := result[t]; ok {
- result[t] = append(result[t], ext)
- } else {
- result[t] = []string{ext}
- }
- }
- } else {
- for ext, t := range m {
- extUpper := strings.ToUpper(ext)
- if _, ok := result[t]; ok {
- result[t] = append(result[t], ext, extUpper)
- } else {
- result[t] = []string{ext, extUpper}
- }
- }
- }
-
- return result
-}
diff --git a/pkg/fs/file_ext.go b/pkg/fs/file_ext.go
new file mode 100644
index 000000000..0c90a2c95
--- /dev/null
+++ b/pkg/fs/file_ext.go
@@ -0,0 +1,65 @@
+package fs
+
+import (
+ "path/filepath"
+ "strings"
+)
+
+const (
+ ExtYAML = ".yml"
+ ExtJPEG = ".jpg"
+ ExtAVC = ".avc"
+)
+
+// Ext returns all extension of a file name including the dots.
+func Ext(name string) string {
+ ext := filepath.Ext(name)
+ name = StripExt(name)
+
+ if Extensions.Known(name) {
+ ext = filepath.Ext(name) + ext
+ }
+
+ return ext
+}
+
+// NormalizedExt returns the file extension without dot and in lowercase.
+func NormalizedExt(fileName string) string {
+ if dot := strings.LastIndex(fileName, "."); dot != -1 && len(fileName[dot+1:]) >= 1 {
+ return strings.ToLower(fileName[dot+1:])
+ }
+
+ return ""
+}
+
+// LowerExt returns the file name extension with dot in lower case.
+func LowerExt(fileName string) string {
+ if fileName == "" {
+ return ""
+ }
+
+ return strings.ToLower(filepath.Ext(fileName))
+}
+
+// TrimExt removes unwanted characters from file extension strings, and makes it lowercase for comparison.
+func TrimExt(ext string) string {
+ return strings.ToLower(strings.Trim(ext, " .,;:“”'`\""))
+}
+
+// StripExt removes the file type extension from a file name (if any).
+func StripExt(name string) string {
+ if end := strings.LastIndex(name, "."); end != -1 {
+ name = name[:end]
+ }
+
+ return name
+}
+
+// StripKnownExt removes all known file type extension from a file name (if any).
+func StripKnownExt(name string) string {
+ for Extensions.Known(name) {
+ name = StripExt(name)
+ }
+
+ return name
+}
diff --git a/pkg/fs/extension_test.go b/pkg/fs/file_ext_test.go
similarity index 53%
rename from pkg/fs/extension_test.go
rename to pkg/fs/file_ext_test.go
index 0401f4e0d..035aae1f9 100644
--- a/pkg/fs/extension_test.go
+++ b/pkg/fs/file_ext_test.go
@@ -6,6 +6,69 @@ import (
"github.com/stretchr/testify/assert"
)
+func TestNormalizeExt(t *testing.T) {
+ t.Run("none", func(t *testing.T) {
+ result := NormalizedExt("testdata/test")
+ assert.Equal(t, "", result)
+ })
+
+ t.Run("dot", func(t *testing.T) {
+ result := NormalizedExt("testdata/test.")
+ assert.Equal(t, "", result)
+ })
+
+ t.Run("test.z", func(t *testing.T) {
+ result := NormalizedExt("testdata/test.z")
+ assert.Equal(t, "z", result)
+ })
+
+ t.Run("test.jpg", func(t *testing.T) {
+ result := NormalizedExt("testdata/test.jpg")
+ assert.Equal(t, "jpg", result)
+ })
+
+ t.Run("test.PNG", func(t *testing.T) {
+ result := NormalizedExt("testdata/test.PNG")
+ assert.Equal(t, "png", result)
+ })
+
+ t.Run("test.MOV", func(t *testing.T) {
+ result := NormalizedExt("testdata/test.MOV")
+ assert.Equal(t, "mov", result)
+ })
+
+ t.Run("test.xmp", func(t *testing.T) {
+ result := NormalizedExt("testdata/test.xMp")
+ assert.Equal(t, "xmp", result)
+ })
+
+ t.Run("test.MP", func(t *testing.T) {
+ result := NormalizedExt("testdata/test.mp")
+ assert.Equal(t, "mp", result)
+ })
+}
+
+func TestTrimExt(t *testing.T) {
+ t.Run("WithDot", func(t *testing.T) {
+ assert.Equal(t, "raf", TrimExt(".raf"))
+ })
+ t.Run("Normalized", func(t *testing.T) {
+ assert.Equal(t, "cr3", TrimExt("cr3"))
+ })
+ t.Run("Uppercase", func(t *testing.T) {
+ assert.Equal(t, "aaf", TrimExt("AAF"))
+ })
+ t.Run("Empty", func(t *testing.T) {
+ assert.Equal(t, "", TrimExt(""))
+ })
+ t.Run("MixedCaseWithDot", func(t *testing.T) {
+ assert.Equal(t, "raw", TrimExt(".Raw"))
+ })
+ t.Run("TypographicQuotes", func(t *testing.T) {
+ assert.Equal(t, "jpeg", TrimExt(" “JPEG” "))
+ })
+}
+
func TestStripExt(t *testing.T) {
t.Run("Test.jpg", func(t *testing.T) {
result := StripExt("/testdata/Test.jpg")
diff --git a/pkg/fs/file_exts.go b/pkg/fs/file_exts.go
new file mode 100644
index 000000000..9dc07e347
--- /dev/null
+++ b/pkg/fs/file_exts.go
@@ -0,0 +1,160 @@
+package fs
+
+import (
+ "path/filepath"
+ "strings"
+)
+
+// FileExtensions maps file extensions to standard formats
+type FileExtensions map[string]Type
+
+// Extensions contains the filename extensions of file formats known to PhotoPrism.
+var Extensions = FileExtensions{
+ ".jpg": ImageJPEG,
+ ".jpeg": ImageJPEG,
+ ".jpe": ImageJPEG,
+ ".jif": ImageJPEG,
+ ".jfif": ImageJPEG,
+ ".jfi": ImageJPEG,
+ ".thm": ImageJPEG,
+ ".heif": ImageHEIF,
+ ".heic": ImageHEIF,
+ ".heifs": ImageHEIF,
+ ".heics": ImageHEIF,
+ ".avci": ImageHEIF,
+ ".avcs": ImageHEIF,
+ ".avif": ImageHEIF,
+ ".avifs": ImageHEIF,
+ ".webp": ImageWebP,
+ ".tif": ImageTIFF,
+ ".tiff": ImageTIFF,
+ ".png": ImagePNG,
+ ".pn": ImagePNG,
+ ".mpo": ImageMPO,
+ ".gif": ImageGIF,
+ ".bmp": ImageBMP,
+ ".3fr": RawImage,
+ ".ari": RawImage,
+ ".arw": RawImage,
+ ".bay": RawImage,
+ ".cap": RawImage,
+ ".crw": RawImage,
+ ".cr2": RawImage,
+ ".cr3": RawImage,
+ ".data": RawImage,
+ ".dcs": RawImage,
+ ".dcr": RawImage,
+ ".dng": RawImage,
+ ".drf": RawImage,
+ ".eip": RawImage,
+ ".erf": RawImage,
+ ".fff": RawImage,
+ ".gpr": RawImage,
+ ".iiq": RawImage,
+ ".k25": RawImage,
+ ".kdc": RawImage,
+ ".mdc": RawImage,
+ ".mef": RawImage,
+ ".mos": RawImage,
+ ".mrw": RawImage,
+ ".nef": RawImage,
+ ".nrw": RawImage,
+ ".obm": RawImage,
+ ".orf": RawImage,
+ ".pef": RawImage,
+ ".ptx": RawImage,
+ ".pxn": RawImage,
+ ".r3d": RawImage,
+ ".raf": RawImage,
+ ".raw": RawImage,
+ ".rwl": RawImage,
+ ".rwz": RawImage,
+ ".rw2": RawImage,
+ ".srf": RawImage,
+ ".srw": RawImage,
+ ".sr2": RawImage,
+ ".x3f": RawImage,
+ ".hevc": VideoHEVC,
+ ".mov": VideoMOV,
+ ".qt": VideoMOV,
+ ".avi": VideoAVI,
+ ".av1": VideoAV1,
+ ".avc": VideoAVC,
+ ".vvc": VideoVVC,
+ ".mpg": VideoMPG,
+ ".mpeg": VideoMPG,
+ ".mjpg": VideoMJPG,
+ ".mjpeg": VideoMJPG,
+ ".mp2": VideoMP2,
+ ".mpv": VideoMP2,
+ ".mp": VideoMP4,
+ ".mp4": VideoMP4,
+ ".m4v": VideoMP4,
+ ".3gp": Video3GP,
+ ".3g2": Video3G2,
+ ".flv": VideoFlash,
+ ".f4v": VideoFlash,
+ ".mkv": VideoMKV,
+ ".mts": VideoAVCHD,
+ ".ogv": VideoOGV,
+ ".ogg": VideoOGV,
+ ".ogx": VideoOGV,
+ ".webm": VideoWebM,
+ ".asf": VideoASF,
+ ".wmv": VideoWMV,
+ ".xmp": XmpFile,
+ ".aae": AaeFile,
+ ".xml": XmlFile,
+ ".yml": YamlFile,
+ ".yaml": YamlFile,
+ ".json": JsonFile,
+ ".toml": TomlFile,
+ ".txt": TextFile,
+ ".md": MarkdownFile,
+ ".markdown": MarkdownFile,
+}
+
+// Known tests if the file extension is known (supported).
+func (m FileExtensions) Known(name string) bool {
+ if name == "" {
+ return false
+ }
+
+ ext := strings.ToLower(filepath.Ext(name))
+
+ if ext == "" {
+ return false
+ }
+
+ if _, ok := m[ext]; ok {
+ return true
+ }
+
+ return false
+}
+
+// TypesExt returns known extensions by file type.
+func (m FileExtensions) Types(noUppercase bool) TypesExt {
+ result := make(TypesExt)
+
+ if noUppercase {
+ for ext, t := range m {
+ if _, ok := result[t]; ok {
+ result[t] = append(result[t], ext)
+ } else {
+ result[t] = []string{ext}
+ }
+ }
+ } else {
+ for ext, t := range m {
+ extUpper := strings.ToUpper(ext)
+ if _, ok := result[t]; ok {
+ result[t] = append(result[t], ext, extUpper)
+ } else {
+ result[t] = []string{ext, extUpper}
+ }
+ }
+ }
+
+ return result
+}
diff --git a/pkg/fs/extensions_test.go b/pkg/fs/file_exts_test.go
similarity index 94%
rename from pkg/fs/extensions_test.go
rename to pkg/fs/file_exts_test.go
index 87ffcd0fc..1b5d916d4 100644
--- a/pkg/fs/extensions_test.go
+++ b/pkg/fs/file_exts_test.go
@@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestFileExt_Known(t *testing.T) {
+func TestFileExtensions_Known(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
assert.False(t, Extensions.Known(""))
})
diff --git a/pkg/fs/file_formats.go b/pkg/fs/file_formats.go
deleted file mode 100644
index c75b45af8..000000000
--- a/pkg/fs/file_formats.go
+++ /dev/null
@@ -1,166 +0,0 @@
-package fs
-
-import (
- _ "image/gif"
- _ "image/jpeg"
- _ "image/png"
- "sort"
- "strings"
- "unicode"
-
- _ "golang.org/x/image/bmp"
- _ "golang.org/x/image/tiff"
- _ "golang.org/x/image/webp"
-)
-
-// FileFormats maps standard formats to file extensions.
-type FileFormats map[Format][]string
-
-// Formats contains the default file type extensions.
-var Formats = Extensions.Formats(ignoreCase)
-
-// Supported file formats.
-const (
- FormatJpeg Format = "jpg" // JPEG image file.
- FormatPng Format = "png" // PNG image file.
- FormatGif Format = "gif" // GIF image file.
- FormatTiff Format = "tiff" // TIFF image file.
- FormatBitmap Format = "bmp" // BMP image file.
- FormatRaw Format = "raw" // RAW image file.
- FormatMpo Format = "mpo" // Stereoscopic Image that consists of two JPG images that are combined into one 3D image
- FormatHEIF Format = "heif" // High Efficiency Image File Format
- FormatWebP Format = "webp" // Google WebP Image
- FormatWebM Format = "webm" // Google WebM Video
- FormatHEVC Format = "hevc" // H.265, High Efficiency Video Coding (HEVC)
- FormatAVC Format = "avc" // H.264, Advanced Video Coding (AVC), MPEG-4 Part 10, used internally
- FormatAV1 Format = "av1" // Alliance for Open Media Video
- FormatMPEG Format = "mpg" // Moving Picture Experts Group (MPEG)
- FormatMJPEG Format = "mjpg" // Motion JPEG (M-JPEG)
- FormatMov Format = "mov" // QuickTime File Format, can contain AVC, HEVC,...
- FormatMp2 Format = "mp2" // MPEG-2, H.222/H.262
- FormatMp4 Format = "mp4" // MPEG-4 Container based on QuickTime, can contain AVC, HEVC,...
- FormatAvi Format = "avi" // Microsoft Audio Video Interleave (AVI)
- Format3gp Format = "3gp" // Mobile Multimedia Container, MPEG-4 Part 12
- Format3g2 Format = "3g2" // Similar to 3GP, consumes less space & bandwidth
- FormatFlv Format = "flv" // Flash Video
- FormatMkv Format = "mkv" // Matroska Multimedia Container, free and open
- FormatMts Format = "mts" // AVCHD (Advanced Video Coding High Definition)
- FormatOgv Format = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
- FormatWMV Format = "wmv" // Windows Media Video
- FormatXMP Format = "xmp" // Adobe XMP sidecar file (XML).
- FormatAAE Format = "aae" // Apple sidecar file (XML).
- FormatXML Format = "xml" // XML metadata / config / sidecar file.
- FormatYaml Format = "yml" // YAML metadata / config / sidecar file.
- FormatToml Format = "toml" // Tom's Obvious, Minimal Language sidecar file.
- FormatJson Format = "json" // JSON metadata / config / sidecar file.
- FormatText Format = "txt" // Text config / sidecar file.
- FormatMarkdown Format = "md" // Markdown text sidecar file.
- FormatOther Format = "" // Unknown file format.
-)
-
-// FormatDesc contains human-readable descriptions for supported file formats
-var FormatDesc = map[Format]string{
- FormatJpeg: "Joint Photographic Experts Group (JPEG)",
- FormatPng: "Portable Network Graphics",
- FormatGif: "Graphics Interchange Format",
- FormatTiff: "Tag Image File Format",
- FormatBitmap: "Bitmap",
- FormatRaw: "Unprocessed Sensor Data",
- FormatMpo: "Stereoscopic (3D JPEG)",
- FormatHEIF: "High Efficiency Image File Format (HEIF)",
- FormatWebP: "Google WebP",
- FormatWebM: "Google WebM",
- FormatHEVC: "High Efficiency Video Coding (HEVC, HVC1, H.265)",
- FormatAVC: "Advanced Video Coding (AVC, AVC1, H.264, MPEG-4 Part 10)",
- FormatAV1: "AOMedia Video 1 (AV1, AV01)",
- FormatMov: "Apple QuickTime (QT)",
- FormatMp2: "MPEG 2 (H.262, H.222)",
- FormatMp4: "Multimedia Container (MPEG-4 Part 14)",
- FormatAvi: "Microsoft Audio Video Interleave",
- FormatWMV: "Microsoft Windows Media",
- Format3gp: "Mobile Multimedia Container (MPEG-4 Part 12)",
- Format3g2: "Mobile Multimedia Container for CDMA2000 (based on 3GP)",
- FormatFlv: "Flash Video",
- FormatMkv: "Matroska Multimedia Container (MKV, MCF, EBML)",
- FormatMPEG: "Moving Picture Experts Group (MPEG)",
- FormatMJPEG: "Motion JPEG (M-JPEG)",
- FormatMts: "Advanced Video Coding High Definition (AVCHD)",
- FormatOgv: "Ogg Media by Xiph.Org",
- FormatXMP: "Adobe Extensible Metadata Platform",
- FormatAAE: "Apple Image Edits",
- FormatXML: "Extensible Markup Language",
- FormatJson: "Serialized JSON Data (Exiftool, Google Photos)",
- FormatYaml: "Serialized YAML Data (Config, Metadata)",
- FormatToml: "Serialized TOML Data (Tom's Obvious, Minimal Language)",
- FormatText: "Plain Text",
- FormatMarkdown: "Markdown Formatted Text",
- FormatOther: "Other",
-}
-
-// Report returns a file format documentation table.
-func (m FileFormats) Report(withDesc, withType, withExt bool) (rows [][]string, cols []string) {
- cols = make([]string, 0, 4)
- cols = append(cols, "Format")
-
- t := 0
-
- if withDesc {
- cols = append(cols, "Description")
- }
-
- if withType {
- if withDesc {
- t = 2
- } else {
- t = 1
- }
-
- cols = append(cols, "Type")
- }
-
- if withExt {
- cols = append(cols, "Extensions")
- }
-
- rows = make([][]string, 0, len(m))
-
- ucFirst := func(str string) string {
- for i, v := range str {
- return string(unicode.ToUpper(v)) + str[i+1:]
- }
- return ""
- }
-
- for f, ext := range m {
- sort.Slice(ext, func(i, j int) bool {
- return ext[i] < ext[j]
- })
-
- v := make([]string, 0, 4)
- v = append(v, strings.ToUpper(f.String()))
-
- if withDesc {
- v = append(v, FormatDesc[f])
- }
-
- if withType {
- v = append(v, ucFirst(string(MediaTypes[f])))
- }
-
- if withExt {
- v = append(v, strings.Join(ext, ", "))
- }
-
- rows = append(rows, v)
- }
-
- sort.Slice(rows, func(i, j int) bool {
- if t > 0 && rows[i][t] == rows[j][t] {
- return rows[i][0] < rows[j][0]
- } else {
- return rows[i][t] < rows[j][t]
- }
- })
-
- return rows, cols
-}
diff --git a/pkg/fs/file_formats_test.go b/pkg/fs/file_formats_test.go
deleted file mode 100644
index f8eb26871..000000000
--- a/pkg/fs/file_formats_test.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package fs
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestFileFormats_Markdown(t *testing.T) {
- t.Run("All", func(t *testing.T) {
- f := Extensions.Formats(true)
- rows, cols := f.Report(true, true, true)
- assert.NotEmpty(t, rows)
- assert.NotEmpty(t, cols)
- assert.Len(t, cols, 4)
- assert.GreaterOrEqual(t, len(rows), 30)
- })
- t.Run("Compact", func(t *testing.T) {
- f := Extensions.Formats(true)
- rows, cols := f.Report(false, false, false)
- assert.NotEmpty(t, rows)
- assert.NotEmpty(t, cols)
- assert.Len(t, cols, 1)
- assert.GreaterOrEqual(t, len(rows), 30)
- })
-}
diff --git a/pkg/fs/file_mediatype.go b/pkg/fs/file_mediatype.go
deleted file mode 100644
index aa4d25a9f..000000000
--- a/pkg/fs/file_mediatype.go
+++ /dev/null
@@ -1,81 +0,0 @@
-package fs
-
-// MediaType represents a general media type.
-type MediaType string
-
-// General categories of media file types.
-const (
- MediaImage MediaType = "image"
- MediaSidecar MediaType = "sidecar"
- MediaRaw MediaType = "raw"
- MediaVideo MediaType = "video"
- MediaVector MediaType = "vector"
- MediaOther MediaType = "other"
-)
-
-// String returns the media name as string.
-func (m MediaType) String() string {
- return string(m)
-}
-
-// MediaTypes maps file formats to general media types.
-var MediaTypes = map[Format]MediaType{
- FormatRaw: MediaRaw,
- FormatJpeg: MediaImage,
- FormatPng: MediaImage,
- FormatGif: MediaImage,
- FormatTiff: MediaImage,
- FormatBitmap: MediaImage,
- FormatMpo: MediaImage,
- FormatHEIF: MediaImage,
- FormatHEVC: MediaVideo,
- FormatWebP: MediaImage,
- FormatWebM: MediaVideo,
- FormatAvi: MediaVideo,
- FormatAVC: MediaVideo,
- FormatAV1: MediaVideo,
- FormatMPEG: MediaVideo,
- FormatMJPEG: MediaVideo,
- FormatMp2: MediaVideo,
- FormatMp4: MediaVideo,
- FormatMkv: MediaVideo,
- FormatMov: MediaVideo,
- Format3gp: MediaVideo,
- Format3g2: MediaVideo,
- FormatFlv: MediaVideo,
- FormatMts: MediaVideo,
- FormatOgv: MediaVideo,
- FormatWMV: MediaVideo,
- FormatXMP: MediaSidecar,
- FormatXML: MediaSidecar,
- FormatAAE: MediaSidecar,
- FormatYaml: MediaSidecar,
- FormatText: MediaSidecar,
- FormatJson: MediaSidecar,
- FormatToml: MediaSidecar,
- FormatMarkdown: MediaSidecar,
- FormatOther: MediaOther,
-}
-
-func GetMediaType(fileName string) MediaType {
- if fileName == "" {
- return MediaOther
- }
-
- result, ok := MediaTypes[FileFormat(fileName)]
-
- if !ok {
- result = MediaOther
- }
-
- return result
-}
-
-func IsMedia(fileName string) bool {
- switch GetMediaType(fileName) {
- case MediaRaw, MediaImage, MediaVideo:
- return true
- default:
- return false
- }
-}
diff --git a/pkg/fs/file_mediatype_test.go b/pkg/fs/file_mediatype_test.go
deleted file mode 100644
index 2f3576991..000000000
--- a/pkg/fs/file_mediatype_test.go
+++ /dev/null
@@ -1,49 +0,0 @@
-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("video", func(t *testing.T) {
- result := GetMediaType("testdata/gopher.mp4")
- assert.Equal(t, MediaVideo, 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)
- })
-
- t.Run("invalid type", func(t *testing.T) {
- result := GetMediaType("/IMG_4120.XXX")
- assert.Equal(t, MediaOther, result)
- })
-}
-
-func TestIsMedia(t *testing.T) {
- t.Run("true", func(t *testing.T) {
- assert.True(t, IsMedia("testdata/test.jpg"))
- })
-
- t.Run("false", func(t *testing.T) {
- assert.False(t, IsMedia("/IMG_4120.XXX"))
- })
-}
diff --git a/pkg/fs/file_format.go b/pkg/fs/file_type.go
similarity index 75%
rename from pkg/fs/file_format.go
rename to pkg/fs/file_type.go
index 547daaee2..5cae771ef 100644
--- a/pkg/fs/file_format.go
+++ b/pkg/fs/file_type.go
@@ -7,30 +7,46 @@ import (
"strings"
)
-// Format represents a file format type.
-type Format string
-
-// String returns the file format as string.
-func (f Format) String() string {
- return string(f)
-}
-
-// Is checks if the format strings match.
-func (f Format) Is(s string) bool {
- if s == "" {
- return false
+// FileType returns the type associated with the specified filename,
+// and UnknownType if it could not be matched.
+func FileType(fileName string) Type {
+ if t, found := Extensions[LowerExt(fileName)]; found {
+ return t
}
- return f.String() == strings.ToLower(s)
+ return UnknownType
}
-// Ext returns the standard file format extension.
-func (f Format) Ext() string {
- return fmt.Sprintf(".%s", f)
+// NewType creates a new file type from a filename extension.
+func NewType(ext string) Type {
+ return Type(TrimExt(ext))
+}
+
+// Type represents a file format type.
+type Type string
+
+// String returns the file format as string.
+func (t Type) String() string {
+ return string(t)
+}
+
+// Equal checks if the type matches.
+func (t Type) Equal(s string) bool {
+ return strings.EqualFold(s, t.String())
+}
+
+// NotEqual checks if the type is different.
+func (t Type) NotEqual(s string) bool {
+ return !t.Equal(s)
+}
+
+// DefaultExt returns the default file format extension with dot.
+func (t Type) DefaultExt() string {
+ return fmt.Sprintf(".%s", t)
}
// Find returns the first filename with the same base name and a given type.
-func (f Format) Find(fileName string, stripSequence bool) string {
+func (t Type) Find(fileName string, stripSequence bool) string {
base := BasePrefix(fileName, stripSequence)
dir := filepath.Dir(fileName)
@@ -38,7 +54,7 @@ func (f Format) Find(fileName string, stripSequence bool) string {
prefixLower := filepath.Join(dir, strings.ToLower(base))
prefixUpper := filepath.Join(dir, strings.ToUpper(base))
- for _, ext := range Formats[f] {
+ for _, ext := range FileTypes[t] {
if info, err := os.Stat(prefix + ext); err == nil && info.Mode().IsRegular() {
return filepath.Join(dir, info.Name())
}
@@ -60,7 +76,7 @@ func (f Format) Find(fileName string, stripSequence bool) string {
}
// FindFirst searches a list of directories for the first file with the same base name and a given type.
-func (f Format) FindFirst(fileName string, dirs []string, baseDir string, stripSequence bool) string {
+func (t Type) FindFirst(fileName string, dirs []string, baseDir string, stripSequence bool) string {
fileBase := filepath.Base(fileName)
fileBasePrefix := BasePrefix(fileName, stripSequence)
fileBaseLower := strings.ToLower(fileBasePrefix)
@@ -69,7 +85,7 @@ func (f Format) FindFirst(fileName string, dirs []string, baseDir string, stripS
fileDir := filepath.Dir(fileName)
search := append([]string{fileDir}, dirs...)
- for _, ext := range Formats[f] {
+ for _, ext := range FileTypes[t] {
lastDir := ""
for _, dir := range search {
@@ -109,7 +125,7 @@ func (f Format) FindFirst(fileName string, dirs []string, baseDir string, stripS
}
// FindAll searches a list of directories for files with the same base name and a given type.
-func (f Format) FindAll(fileName string, dirs []string, baseDir string, stripSequence bool) (results []string) {
+func (t Type) FindAll(fileName string, dirs []string, baseDir string, stripSequence bool) (results []string) {
fileBase := filepath.Base(fileName)
fileBasePrefix := BasePrefix(fileName, stripSequence)
fileBaseLower := strings.ToLower(fileBasePrefix)
@@ -118,7 +134,7 @@ func (f Format) FindAll(fileName string, dirs []string, baseDir string, stripSeq
fileDir := filepath.Dir(fileName)
search := append([]string{fileDir}, dirs...)
- for _, ext := range Formats[f] {
+ for _, ext := range FileTypes[t] {
lastDir := ""
for _, dir := range search {
@@ -160,15 +176,3 @@ func (f Format) FindAll(fileName string, dirs []string, baseDir string, stripSeq
return results
}
-
-// FileFormat returns the (expected) type for a given file name.
-func FileFormat(fileName string) Format {
- fileExt := strings.ToLower(filepath.Ext(fileName))
- result, ok := Extensions[fileExt]
-
- if !ok {
- result = FormatOther
- }
-
- return result
-}
diff --git a/pkg/fs/file_format_test.go b/pkg/fs/file_type_test.go
similarity index 50%
rename from pkg/fs/file_format_test.go
rename to pkg/fs/file_type_test.go
index 3722e3461..40895bfc4 100644
--- a/pkg/fs/file_format_test.go
+++ b/pkg/fs/file_type_test.go
@@ -6,148 +6,166 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestFileFormat_String(t *testing.T) {
+func TestType_String(t *testing.T) {
t.Run("jpg", func(t *testing.T) {
- assert.Equal(t, "jpg", FormatJpeg.String())
+ assert.Equal(t, "jpg", ImageJPEG.String())
})
}
-func TestFileFormat_Is(t *testing.T) {
+func TestType_Equal(t *testing.T) {
+ t.Run("jpg", func(t *testing.T) {
+ assert.True(t, ImageJPEG.Equal("jpg"))
+ })
+}
+
+func TestToType(t *testing.T) {
+ t.Run("jpg", func(t *testing.T) {
+ assert.Equal(t, "jpg", NewType("JPG").String())
+ })
+ t.Run("JPEG", func(t *testing.T) {
+ assert.Equal(t, Type("jpeg"), NewType("JPEG"))
+ })
+ t.Run(".jpg", func(t *testing.T) {
+ assert.Equal(t, "jpg", NewType(".jpg").String())
+ })
+}
+
+func TestType_Is(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
- assert.False(t, FormatJpeg.Is(""))
+ assert.False(t, ImageJPEG.Equal(""))
})
t.Run("Upper", func(t *testing.T) {
- assert.True(t, FormatJpeg.Is("JPG"))
+ assert.True(t, ImageJPEG.Equal("JPG"))
})
t.Run("Lower", func(t *testing.T) {
- assert.True(t, FormatJpeg.Is("jpg"))
+ assert.True(t, ImageJPEG.Equal("jpg"))
})
t.Run("False", func(t *testing.T) {
- assert.False(t, FormatJpeg.Is("raw"))
+ assert.False(t, ImageJPEG.Equal("raw"))
})
}
-func TestFileFormat_Find(t *testing.T) {
+func TestType_Find(t *testing.T) {
t.Run("find jpg", func(t *testing.T) {
- result := FormatJpeg.Find("testdata/test.xmp", false)
+ result := ImageJPEG.Find("testdata/test.xmp", false)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("upper ext", func(t *testing.T) {
- result := FormatJpeg.Find("testdata/test.XMP", false)
+ result := ImageJPEG.Find("testdata/test.XMP", false)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("with sequence", func(t *testing.T) {
- result := FormatJpeg.Find("testdata/test (2).xmp", false)
+ result := ImageJPEG.Find("testdata/test (2).xmp", false)
assert.Equal(t, "", result)
})
t.Run("strip sequence", func(t *testing.T) {
- result := FormatJpeg.Find("testdata/test (2).xmp", true)
+ result := ImageJPEG.Find("testdata/test (2).xmp", true)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("name upper", func(t *testing.T) {
- result := FormatJpeg.Find("testdata/CATYELLOW.xmp", true)
+ result := ImageJPEG.Find("testdata/CATYELLOW.xmp", true)
assert.Equal(t, "testdata/CATYELLOW.jpg", result)
})
t.Run("name lower", func(t *testing.T) {
- result := FormatJpeg.Find("testdata/chameleon_lime.xmp", true)
+ result := ImageJPEG.Find("testdata/chameleon_lime.xmp", true)
assert.Equal(t, "testdata/chameleon_lime.jpg", result)
})
}
-func TestFileFormat_FindFirst(t *testing.T) {
+func TestType_FindFirst(t *testing.T) {
dirs := []string{HiddenPath}
t.Run("find xmp", func(t *testing.T) {
- result := FormatXMP.FindFirst("testdata/test.jpg", dirs, "", false)
+ result := XmpFile.FindFirst("testdata/test.jpg", dirs, "", false)
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
})
t.Run("find xmp upper ext", func(t *testing.T) {
- result := FormatXMP.FindFirst("testdata/test.PNG", dirs, "", false)
+ result := XmpFile.FindFirst("testdata/test.PNG", dirs, "", false)
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
})
t.Run("find xmp without sequence", func(t *testing.T) {
- result := FormatXMP.FindFirst("testdata/test (2).jpg", dirs, "", false)
+ result := XmpFile.FindFirst("testdata/test (2).jpg", dirs, "", false)
assert.Equal(t, "", result)
})
t.Run("find xmp with sequence", func(t *testing.T) {
- result := FormatXMP.FindFirst("testdata/test (2).jpg", dirs, "", true)
+ result := XmpFile.FindFirst("testdata/test (2).jpg", dirs, "", true)
assert.Equal(t, "testdata/.photoprism/test.xmp", result)
})
t.Run("find jpg", func(t *testing.T) {
- result := FormatJpeg.FindFirst("testdata/test.xmp", dirs, "", false)
+ result := ImageJPEG.FindFirst("testdata/test.xmp", dirs, "", false)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("find jpg abs", func(t *testing.T) {
- result := FormatJpeg.FindFirst(Abs("testdata/test.xmp"), dirs, "", false)
+ result := ImageJPEG.FindFirst(Abs("testdata/test.xmp"), dirs, "", false)
assert.Equal(t, Abs("testdata/test.jpg"), result)
})
t.Run("upper ext", func(t *testing.T) {
- result := FormatJpeg.FindFirst("testdata/test.XMP", dirs, "", false)
+ result := ImageJPEG.FindFirst("testdata/test.XMP", dirs, "", false)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("with sequence", func(t *testing.T) {
- result := FormatJpeg.FindFirst("testdata/test (2).xmp", dirs, "", false)
+ result := ImageJPEG.FindFirst("testdata/test (2).xmp", dirs, "", false)
assert.Equal(t, "", result)
})
t.Run("strip sequence", func(t *testing.T) {
- result := FormatJpeg.FindFirst("testdata/test (2).xmp", dirs, "", true)
+ result := ImageJPEG.FindFirst("testdata/test (2).xmp", dirs, "", true)
assert.Equal(t, "testdata/test.jpg", result)
})
t.Run("name upper", func(t *testing.T) {
- result := FormatJpeg.FindFirst("testdata/CATYELLOW.xmp", dirs, "", true)
+ result := ImageJPEG.FindFirst("testdata/CATYELLOW.xmp", dirs, "", true)
assert.Equal(t, "testdata/CATYELLOW.jpg", result)
})
t.Run("name lower", func(t *testing.T) {
- result := FormatJpeg.FindFirst("testdata/chameleon_lime.xmp", dirs, "", true)
+ result := ImageJPEG.FindFirst("testdata/chameleon_lime.xmp", dirs, "", true)
assert.Equal(t, "testdata/chameleon_lime.jpg", result)
})
t.Run("example_bmp_notfound", func(t *testing.T) {
- result := FormatBitmap.FindFirst("testdata/example.00001.jpg", dirs, "", true)
+ result := ImageBMP.FindFirst("testdata/example.00001.jpg", dirs, "", true)
assert.Equal(t, "", result)
})
t.Run("example_bmp_found", func(t *testing.T) {
- result := FormatBitmap.FindFirst("testdata/example.00001.jpg", []string{"directory"}, "", true)
+ result := ImageBMP.FindFirst("testdata/example.00001.jpg", []string{"directory"}, "", true)
assert.Equal(t, "testdata/directory/example.bmp", result)
})
t.Run("example_png_found", func(t *testing.T) {
- result := FormatPng.FindFirst("testdata/example.00001.jpg", []string{"directory", "directory/subdirectory"}, "", true)
+ result := ImagePNG.FindFirst("testdata/example.00001.jpg", []string{"directory", "directory/subdirectory"}, "", true)
assert.Equal(t, "testdata/directory/subdirectory/example.png", result)
})
t.Run("example_bmp_found", func(t *testing.T) {
- result := FormatBitmap.FindFirst(Abs("testdata/example.00001.jpg"), []string{"directory"}, Abs("testdata"), true)
+ result := ImageBMP.FindFirst(Abs("testdata/example.00001.jpg"), []string{"directory"}, Abs("testdata"), true)
assert.Equal(t, Abs("testdata/directory/example.bmp"), result)
})
}
-func TestFileFormat_FindAll(t *testing.T) {
+func TestType_FindAll(t *testing.T) {
dirs := []string{HiddenPath}
t.Run("CATYELLOW.jpg", func(t *testing.T) {
- result := FormatJpeg.FindAll("testdata/CATYELLOW.JSON", dirs, "", false)
+ result := ImageJPEG.FindAll("testdata/CATYELLOW.JSON", dirs, "", false)
assert.Contains(t, result, "testdata/CATYELLOW.jpg")
})
}
-func TestFileFormat(t *testing.T) {
+func TestType(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
- result := FileFormat("")
- assert.Equal(t, FormatOther, result)
+ result := FileType("")
+ assert.Equal(t, UnknownType, result)
})
t.Run("JPEG", func(t *testing.T) {
- result := FileFormat("testdata/test.jpg")
- assert.Equal(t, FormatJpeg, result)
+ result := FileType("testdata/test.jpg")
+ assert.Equal(t, ImageJPEG, result)
})
t.Run("RawCRw", func(t *testing.T) {
- result := FileFormat("testdata/test (jpg).crw")
- assert.Equal(t, FormatRaw, result)
+ result := FileType("testdata/test (jpg).crw")
+ assert.Equal(t, RawImage, result)
})
t.Run("RawCR2", func(t *testing.T) {
- result := FileFormat("testdata/test (jpg).CR2")
- assert.Equal(t, FormatRaw, result)
+ result := FileType("testdata/test (jpg).CR2")
+ assert.Equal(t, RawImage, result)
})
t.Run("MP4", func(t *testing.T) {
- assert.Equal(t, Format("mp4"), FileFormat("file.mp"))
+ assert.Equal(t, Type("mp4"), FileType("file.mp"))
})
}
diff --git a/pkg/fs/file_types.go b/pkg/fs/file_types.go
new file mode 100644
index 000000000..34dd3aff9
--- /dev/null
+++ b/pkg/fs/file_types.go
@@ -0,0 +1,93 @@
+package fs
+
+import (
+ _ "image/gif"
+ _ "image/jpeg"
+ _ "image/png"
+
+ _ "golang.org/x/image/bmp"
+ _ "golang.org/x/image/tiff"
+ _ "golang.org/x/image/webp"
+)
+
+// File types.
+const (
+ RawImage Type = "raw" // RAW image file.
+ ImageJPEG Type = "jpg" // JPEG image file.
+ ImageHEIF Type = "heif" // High Efficiency Image File Format
+ ImageTIFF Type = "tiff" // TIFF image file.
+ ImagePNG Type = "png" // PNG image file.
+ ImageGIF Type = "gif" // GIF image file.
+ ImageBMP Type = "bmp" // BMP image file.
+ ImageMPO Type = "mpo" // Stereoscopic Image that consists of two JPG images that are combined into one 3D image
+ ImageWebP Type = "webp" // Google WebP Image
+ VideoWebM Type = "webm" // Google WebM Video
+ VideoAVC Type = "avc" // H.264, Advanced Video Coding (AVC, MPEG-4 Part 10)
+ VideoHEVC Type = "hevc" // H.265, High Efficiency Video Coding (HEVC)
+ VideoVVC Type = "vvc" // H.266, Versatile Video Coding (VVC)
+ VideoAV1 Type = "av1" // Alliance for Open Media Video
+ VideoMPG Type = "mpg" // Moving Picture Experts Group (MPEG)
+ VideoMJPG Type = "mjpg" // Motion JPEG (M-JPEG)
+ VideoMOV Type = "mov" // QuickTime File Format, can contain AVC, HEVC,...
+ VideoMP2 Type = "mp2" // MPEG-2, H.222/H.262
+ VideoMP4 Type = "mp4" // MPEG-4 Container based on QuickTime, can contain AVC, HEVC,...
+ VideoAVI Type = "avi" // Microsoft Audio Video Interleave (AVI)
+ Video3GP Type = "3gp" // Mobile Multimedia Container, MPEG-4 Part 12
+ Video3G2 Type = "3g2" // Similar to 3GP, consumes less space & bandwidth
+ VideoFlash Type = "flv" // Flash Video
+ VideoMKV Type = "mkv" // Matroska Multimedia Container, free and open
+ VideoAVCHD Type = "mts" // AVCHD (Advanced Video Coding High Definition)
+ VideoOGV Type = "ogv" // Ogg container format maintained by the Xiph.Org, free and open
+ VideoASF Type = "asf" // Advanced Systems/Streaming Format (ASF)
+ VideoWMV Type = "wmv" // Windows Media Video (based on ASF)
+ XmpFile Type = "xmp" // Adobe XMP sidecar file (XML).
+ AaeFile Type = "aae" // Apple image edits sidecar file (based on XML).
+ XmlFile Type = "xml" // XML metadata / config / sidecar file.
+ YamlFile Type = "yml" // YAML metadata / config / sidecar file.
+ TomlFile Type = "toml" // Tom's Obvious, Minimal Language sidecar file.
+ JsonFile Type = "json" // JSON metadata / config / sidecar file.
+ TextFile Type = "txt" // Text config / sidecar file.
+ MarkdownFile Type = "md" // Markdown text sidecar file.
+ UnknownType Type = "" // Unknown file type.
+)
+
+// TypeInfo contains human-readable descriptions for supported file formats
+var TypeInfo = map[Type]string{
+ RawImage: "Unprocessed Sensor Data",
+ ImageJPEG: "Joint Photographic Experts Group (JPEG)",
+ ImagePNG: "Portable Network Graphics",
+ ImageGIF: "Graphics Interchange Format",
+ ImageTIFF: "Tag Image File Format",
+ ImageBMP: "Bitmap",
+ ImageMPO: "Stereoscopic JPEG (3D)",
+ ImageHEIF: "High Efficiency Image File Format (HEIF)",
+ ImageWebP: "Google WebP",
+ VideoWebM: "Google WebM",
+ VideoMP2: "MPEG 2 / H.262",
+ VideoAVC: "Advanced Video Coding (AVC, MPEG-4 Part 10) / H.264",
+ VideoHEVC: "High Efficiency Video Coding (HEVC) / H.265",
+ VideoVVC: "Versatile Video Coding (VVC) / H.266",
+ VideoAV1: "AOMedia Video 1 (AV1)",
+ VideoMOV: "Apple QuickTime (MOV)",
+ VideoMP4: "Multimedia Container (MPEG-4 Part 14)",
+ VideoAVI: "Microsoft Audio Video Interleave (AVI)",
+ VideoASF: "Advanced Systems Format (ASF)",
+ VideoWMV: "Windows Media",
+ Video3GP: "Mobile Multimedia Container (3G)",
+ Video3G2: "Mobile Multimedia Container (CDMA2000)",
+ VideoFlash: "Adobe Flash",
+ VideoMKV: "Matroska Multimedia Container (MKV)",
+ VideoMPG: "Moving Picture Experts Group (MPEG)",
+ VideoMJPG: "Motion JPEG (M-JPEG)",
+ VideoAVCHD: "Advanced Video Coding High Definition (AVCHD)",
+ VideoOGV: "Ogg Media (OGG)",
+ XmpFile: "Adobe Extensible Metadata Platform (XMP)",
+ AaeFile: "Apple Image Edits XML",
+ XmlFile: "Extensible Markup Language (XML)",
+ JsonFile: "Serialized JSON Data (Exiftool, Google Photos)",
+ YamlFile: "Serialized YAML Data (Config, Metadata)",
+ TomlFile: "Serialized TOML Data (Tom's Obvious, Minimal Language)",
+ TextFile: "Plain Text",
+ MarkdownFile: "Markdown Formatted Text",
+ UnknownType: "Other",
+}
diff --git a/pkg/fs/file_types_ext.go b/pkg/fs/file_types_ext.go
new file mode 100644
index 000000000..2ee8fe4e5
--- /dev/null
+++ b/pkg/fs/file_types_ext.go
@@ -0,0 +1,17 @@
+package fs
+
+import (
+ _ "image/gif"
+ _ "image/jpeg"
+ _ "image/png"
+
+ _ "golang.org/x/image/bmp"
+ _ "golang.org/x/image/tiff"
+ _ "golang.org/x/image/webp"
+)
+
+// TypesExt maps standard formats to file extensions.
+type TypesExt map[Type][]string
+
+// FileTypes contains the default file type extensions.
+var FileTypes = Extensions.Types(ignoreCase)
diff --git a/pkg/fs/fs.go b/pkg/fs/fs.go
index 392f9ae0c..60b028dbc 100644
--- a/pkg/fs/fs.go
+++ b/pkg/fs/fs.go
@@ -81,7 +81,7 @@ func PathWritable(path string) bool {
return false
}
- tmpName := filepath.Join(path, "."+rnd.Token(8))
+ tmpName := filepath.Join(path, "."+rnd.GenerateToken(8))
if f, err := os.Create(tmpName); err != nil {
return false
diff --git a/pkg/fs/id.go b/pkg/fs/id.go
index 65f0246ea..6583f920c 100644
--- a/pkg/fs/id.go
+++ b/pkg/fs/id.go
@@ -86,7 +86,7 @@ func IsGenerated(fileName string) bool {
return true
} else if IsUniqueName(base) {
return true
- } else if rnd.IsUID(base, 0) {
+ } else if rnd.ValidID(base, 0) {
return true
} else if IsCanonical(base) {
return true
diff --git a/pkg/fs/mime.go b/pkg/fs/mime.go
index 122789b28..96c757790 100644
--- a/pkg/fs/mime.go
+++ b/pkg/fs/mime.go
@@ -33,7 +33,7 @@ func MimeType(filename string) string {
return ""
} else if t, err := filetype.Get(buffer); err == nil && t != filetype.Unknown {
return t.MIME.Value
- } else if t := filetype.GetType(NormalizeExt(filename)); t != filetype.Unknown {
+ } else if t := filetype.GetType(NormalizedExt(filename)); t != filetype.Unknown {
return t.MIME.Value
} else {
return ""
diff --git a/pkg/fs/walk_test.go b/pkg/fs/walk_test.go
index 63ad29cf7..98cc035b0 100644
--- a/pkg/fs/walk_test.go
+++ b/pkg/fs/walk_test.go
@@ -77,7 +77,7 @@ func TestSkipWalk(t *testing.T) {
done[fileName] = Found
- if textName := FormatText.Find(fileName, false); textName != "" {
+ if textName := TextFile.Find(fileName, false); textName != "" {
done[textName] = Found
}
diff --git a/pkg/media/filename.go b/pkg/media/filename.go
new file mode 100644
index 000000000..705281347
--- /dev/null
+++ b/pkg/media/filename.go
@@ -0,0 +1,25 @@
+package media
+
+import (
+ "github.com/photoprism/photoprism/pkg/fs"
+)
+
+// FromName returns the content type matching the file extension.
+func FromName(fileName string) Type {
+ if fileName == "" {
+ return Unknown
+ }
+
+ // Find media type based on the file type.
+ if result, found := Formats[fs.FileType(fileName)]; found {
+ return result
+ }
+
+ // Default.
+ return Other
+}
+
+// MainFile checks if the filename belongs to a main content type.
+func MainFile(fileName string) bool {
+ return FromName(fileName).Main()
+}
diff --git a/pkg/media/filename_test.go b/pkg/media/filename_test.go
new file mode 100644
index 000000000..9d53bce96
--- /dev/null
+++ b/pkg/media/filename_test.go
@@ -0,0 +1,43 @@
+package media
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFromName(t *testing.T) {
+ t.Run("jpeg", func(t *testing.T) {
+ result := FromName("testdata/test.jpg")
+ assert.Equal(t, Image, result)
+ })
+ t.Run("raw", func(t *testing.T) {
+ result := FromName("testdata/test (jpg).CR2")
+ assert.Equal(t, Raw, result)
+ })
+ t.Run("video", func(t *testing.T) {
+ result := FromName("testdata/gopher.mp4")
+ assert.Equal(t, Video, result)
+ })
+ t.Run("sidecar", func(t *testing.T) {
+ result := FromName("/IMG_4120.AAE")
+ assert.Equal(t, Sidecar, result)
+ })
+ t.Run("empty", func(t *testing.T) {
+ result := FromName("")
+ assert.Equal(t, Unknown, result)
+ })
+ t.Run("invalid type", func(t *testing.T) {
+ result := FromName("/IMG_4120.XXX")
+ assert.Equal(t, Other, result)
+ })
+}
+
+func TestMainFile(t *testing.T) {
+ t.Run("true", func(t *testing.T) {
+ assert.True(t, MainFile("testdata/test.jpg"))
+ })
+ t.Run("false", func(t *testing.T) {
+ assert.False(t, MainFile("/IMG_4120.XXX"))
+ })
+}
diff --git a/pkg/media/formats.go b/pkg/media/formats.go
new file mode 100644
index 000000000..821fe4af9
--- /dev/null
+++ b/pkg/media/formats.go
@@ -0,0 +1,44 @@
+package media
+
+import "github.com/photoprism/photoprism/pkg/fs"
+
+// Formats maps file formats to general media types.
+var Formats = map[fs.Type]Type{
+ fs.RawImage: Raw,
+ fs.ImageJPEG: Image,
+ fs.ImagePNG: Image,
+ fs.ImageGIF: Image,
+ fs.ImageTIFF: Image,
+ fs.ImageBMP: Image,
+ fs.ImageMPO: Image,
+ fs.ImageHEIF: Image,
+ fs.VideoHEVC: Video,
+ fs.ImageWebP: Image,
+ fs.VideoWebM: Video,
+ fs.VideoAVI: Video,
+ fs.VideoAVC: Video,
+ fs.VideoVVC: Video,
+ fs.VideoAV1: Video,
+ fs.VideoMPG: Video,
+ fs.VideoMJPG: Video,
+ fs.VideoMP2: Video,
+ fs.VideoMP4: Video,
+ fs.VideoMKV: Video,
+ fs.VideoMOV: Video,
+ fs.Video3GP: Video,
+ fs.Video3G2: Video,
+ fs.VideoFlash: Video,
+ fs.VideoAVCHD: Video,
+ fs.VideoOGV: Video,
+ fs.VideoASF: Video,
+ fs.VideoWMV: Video,
+ fs.XmpFile: Sidecar,
+ fs.XmlFile: Sidecar,
+ fs.AaeFile: Sidecar,
+ fs.YamlFile: Sidecar,
+ fs.TextFile: Sidecar,
+ fs.JsonFile: Sidecar,
+ fs.TomlFile: Sidecar,
+ fs.MarkdownFile: Sidecar,
+ fs.UnknownType: Other,
+}
diff --git a/pkg/media/media.go b/pkg/media/media.go
new file mode 100644
index 000000000..7ec88d740
--- /dev/null
+++ b/pkg/media/media.go
@@ -0,0 +1,27 @@
+/*
+
+Package media provides general content types and maps them to file formats.
+
+Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
+
+ This program is free software: you can redistribute it and/or modify
+ it under Version 3 of the GNU Affero General Public License (the "AGPL"):
+
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ The AGPL is supplemented by our Trademark and Brand Guidelines,
+ which describe how our Brand Assets may be used:
+
+
+Feel free to send an email to hello@photoprism.app if you have questions,
+want to support our work, or just want to say hello.
+
+Additional information can be found in our Developer Guide:
+
+
+*/
+package media
diff --git a/pkg/media/new.go b/pkg/media/new.go
new file mode 100644
index 000000000..ac56b2822
--- /dev/null
+++ b/pkg/media/new.go
@@ -0,0 +1,8 @@
+package media
+
+import "github.com/photoprism/photoprism/pkg/clean"
+
+// New casts a string to a type.
+func New(s string) Type {
+ return Type(clean.ShortTypeLower(s))
+}
diff --git a/pkg/media/new_test.go b/pkg/media/new_test.go
new file mode 100644
index 000000000..982a95a55
--- /dev/null
+++ b/pkg/media/new_test.go
@@ -0,0 +1,13 @@
+package media
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestNew(t *testing.T) {
+ mediaType := New(" FOO-bar! FOO-bar! 0123 XXXXXXXXXXXXXXXXXXXXXXXXXXXX YYYYY YYYYYYYYY YZ")
+
+ assert.Equal(t, "foo-bar!", mediaType.String())
+}
diff --git a/pkg/media/report.go b/pkg/media/report.go
new file mode 100644
index 000000000..27f6ab375
--- /dev/null
+++ b/pkg/media/report.go
@@ -0,0 +1,77 @@
+package media
+
+import (
+ "sort"
+ "strings"
+ "unicode"
+
+ "github.com/photoprism/photoprism/pkg/fs"
+)
+
+// Report returns a file format documentation table.
+func Report(m fs.TypesExt, withDesc, withType, withExt bool) (rows [][]string, cols []string) {
+ cols = make([]string, 0, 4)
+ cols = append(cols, "Format")
+
+ t := 0
+
+ if withDesc {
+ cols = append(cols, "Description")
+ }
+
+ if withType {
+ if withDesc {
+ t = 2
+ } else {
+ t = 1
+ }
+
+ cols = append(cols, "Type")
+ }
+
+ if withExt {
+ cols = append(cols, "Extensions")
+ }
+
+ rows = make([][]string, 0, len(m))
+
+ ucFirst := func(str string) string {
+ for i, v := range str {
+ return string(unicode.ToUpper(v)) + str[i+1:]
+ }
+ return ""
+ }
+
+ for f, ext := range m {
+ sort.Slice(ext, func(i, j int) bool {
+ return ext[i] < ext[j]
+ })
+
+ v := make([]string, 0, 4)
+ v = append(v, strings.ToUpper(f.String()))
+
+ if withDesc {
+ v = append(v, fs.TypeInfo[f])
+ }
+
+ if withType {
+ v = append(v, ucFirst(string(Formats[f])))
+ }
+
+ if withExt {
+ v = append(v, strings.Join(ext, ", "))
+ }
+
+ rows = append(rows, v)
+ }
+
+ sort.Slice(rows, func(i, j int) bool {
+ if t > 0 && rows[i][t] == rows[j][t] {
+ return rows[i][0] < rows[j][0]
+ } else {
+ return rows[i][t] < rows[j][t]
+ }
+ })
+
+ return rows, cols
+}
diff --git a/pkg/media/type.go b/pkg/media/type.go
new file mode 100644
index 000000000..c6a04edaa
--- /dev/null
+++ b/pkg/media/type.go
@@ -0,0 +1,38 @@
+package media
+
+import (
+ "strings"
+)
+
+// Type represents a general media content type.
+type Type string
+
+// String returns the type as string.
+func (t Type) String() string {
+ return string(t)
+}
+
+// Equal checks if the type matches.
+func (t Type) Equal(s string) bool {
+ return strings.EqualFold(s, t.String())
+}
+
+// NotEqual checks if the type is different.
+func (t Type) NotEqual(s string) bool {
+ return !t.Equal(s)
+}
+
+// Main checks if this is a known main media content format.
+func (t Type) Main() bool {
+ switch t {
+ case Raw, Image, Video, Live, Animated, Vector:
+ return true
+ default:
+ return false
+ }
+}
+
+// Unknown checks if the type is unknown.
+func (t Type) Unknown() bool {
+ return t == Unknown
+}
diff --git a/pkg/media/type_test.go b/pkg/media/type_test.go
new file mode 100644
index 000000000..b46d85ffc
--- /dev/null
+++ b/pkg/media/type_test.go
@@ -0,0 +1,67 @@
+package media
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestType_Main(t *testing.T) {
+ t.Run("Unknown", func(t *testing.T) {
+ assert.False(t, Unknown.Main())
+ })
+ t.Run("Image", func(t *testing.T) {
+ assert.True(t, Image.Main())
+ })
+ t.Run("Video", func(t *testing.T) {
+ assert.True(t, Video.Main())
+ })
+ t.Run("Sidecar", func(t *testing.T) {
+ assert.False(t, Sidecar.Main())
+ })
+}
+
+func TestType_Unknown(t *testing.T) {
+ t.Run("Unknown", func(t *testing.T) {
+ assert.True(t, Unknown.Unknown())
+ })
+ t.Run("Image", func(t *testing.T) {
+ assert.False(t, Image.Unknown())
+ })
+ t.Run("Video", func(t *testing.T) {
+ assert.False(t, Video.Unknown())
+ })
+ t.Run("Sidecar", func(t *testing.T) {
+ assert.False(t, Sidecar.Unknown())
+ })
+}
+
+func TestType_Equal(t *testing.T) {
+ t.Run("UnknownUnknown", func(t *testing.T) {
+ assert.True(t, Unknown.Equal(""))
+ })
+ t.Run("ImageImage", func(t *testing.T) {
+ assert.True(t, Image.Equal(Image.String()))
+ })
+ t.Run("VideoImage", func(t *testing.T) {
+ assert.False(t, Video.Equal(Image.String()))
+ })
+ t.Run("SidecarUnknown", func(t *testing.T) {
+ assert.False(t, Sidecar.Equal(Unknown.String()))
+ })
+}
+
+func TestType_NotEqual(t *testing.T) {
+ t.Run("UnknownUnknown", func(t *testing.T) {
+ assert.False(t, Unknown.NotEqual(""))
+ })
+ t.Run("ImageImage", func(t *testing.T) {
+ assert.False(t, Image.NotEqual(Image.String()))
+ })
+ t.Run("VideoImage", func(t *testing.T) {
+ assert.True(t, Video.NotEqual(Image.String()))
+ })
+ t.Run("SidecarUnknown", func(t *testing.T) {
+ assert.True(t, Sidecar.NotEqual(Unknown.String()))
+ })
+}
diff --git a/pkg/media/types.go b/pkg/media/types.go
new file mode 100644
index 000000000..f8a0d5e5e
--- /dev/null
+++ b/pkg/media/types.go
@@ -0,0 +1,14 @@
+package media
+
+const (
+ Unknown Type = ""
+ Image Type = "image"
+ Raw Type = "raw"
+ Vector Type = "vector"
+ Animated Type = "animated"
+ Live Type = "live"
+ Video Type = "video"
+ Sidecar Type = "sidecar"
+ Text Type = "text"
+ Other Type = "other"
+)
diff --git a/pkg/projection/find.go b/pkg/projection/find.go
new file mode 100644
index 000000000..f173fe60c
--- /dev/null
+++ b/pkg/projection/find.go
@@ -0,0 +1,20 @@
+package projection
+
+import (
+ "github.com/photoprism/photoprism/pkg/clean"
+)
+
+// Find returns the project matching the name.
+func Find(name string) Type {
+ if name == "" {
+ return Unknown
+ }
+
+ // Find known type based on the normalized name.
+ if result, found := Types[clean.TypeLower(name)]; found {
+ return result
+ }
+
+ // Default.
+ return Other
+}
diff --git a/pkg/projection/find_test.go b/pkg/projection/find_test.go
new file mode 100644
index 000000000..eacd9f595
--- /dev/null
+++ b/pkg/projection/find_test.go
@@ -0,0 +1,22 @@
+package projection
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestFromName(t *testing.T) {
+ t.Run("Unknown", func(t *testing.T) {
+ result := Find("")
+ assert.Equal(t, Unknown, result)
+ })
+ t.Run("Other", func(t *testing.T) {
+ result := Find("zdfbhmdflkbhelkthn")
+ assert.Equal(t, Other, result)
+ })
+ t.Run(Equirectangular.String(), func(t *testing.T) {
+ result := Find("Equirectangular ")
+ assert.Equal(t, Equirectangular, result)
+ })
+}
diff --git a/pkg/projection/new.go b/pkg/projection/new.go
new file mode 100644
index 000000000..b87598470
--- /dev/null
+++ b/pkg/projection/new.go
@@ -0,0 +1,8 @@
+package projection
+
+import "github.com/photoprism/photoprism/pkg/clean"
+
+// New creates a projection type.
+func New(s string) Type {
+ return Type(clean.TypeLower(s))
+}
diff --git a/pkg/projection/new_test.go b/pkg/projection/new_test.go
new file mode 100644
index 000000000..5cc4b0f2c
--- /dev/null
+++ b/pkg/projection/new_test.go
@@ -0,0 +1,13 @@
+package projection
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTNew(t *testing.T) {
+ proj := New(" FOO-bar! FOO-bar! 0123 XXXXXXXXXXXXXXXXXXXXXXXXXXXX YYYYY YYYYYYYYY YZ")
+
+ assert.Equal(t, "foo-bar! foo-bar! 0123 xxxxxxxxxxxxxxxxxxxxxxxxxxxx yyyyy", proj.String())
+}
diff --git a/pkg/projection/projection.go b/pkg/projection/projection.go
new file mode 100644
index 000000000..f7af11cf5
--- /dev/null
+++ b/pkg/projection/projection.go
@@ -0,0 +1,27 @@
+/*
+
+Package projection provides visual projection types and methods.
+
+Copyright (c) 2018 - 2022 PhotoPrism UG. All rights reserved.
+
+ This program is free software: you can redistribute it and/or modify
+ it under Version 3 of the GNU Affero General Public License (the "AGPL"):
+
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ The AGPL is supplemented by our Trademark and Brand Guidelines,
+ which describe how our Brand Assets may be used:
+
+
+Feel free to send an email to hello@photoprism.app if you have questions,
+want to support our work, or just want to say hello.
+
+Additional information can be found in our Developer Guide:
+
+
+*/
+package projection
diff --git a/pkg/projection/type.go b/pkg/projection/type.go
new file mode 100644
index 000000000..a9bcae23c
--- /dev/null
+++ b/pkg/projection/type.go
@@ -0,0 +1,28 @@
+package projection
+
+import (
+ "strings"
+)
+
+// Type represents a visual projection type.
+type Type string
+
+// String returns the type as string.
+func (t Type) String() string {
+ return string(t)
+}
+
+// Unknown checks if the type is unknown.
+func (t Type) Unknown() bool {
+ return t == Unknown
+}
+
+// Equal checks if the type matches.
+func (t Type) Equal(s string) bool {
+ return strings.EqualFold(s, t.String())
+}
+
+// NotEqual checks if the type is different.
+func (t Type) NotEqual(s string) bool {
+ return !t.Equal(s)
+}
diff --git a/pkg/projection/type_test.go b/pkg/projection/type_test.go
new file mode 100644
index 000000000..154dadc6d
--- /dev/null
+++ b/pkg/projection/type_test.go
@@ -0,0 +1,37 @@
+package projection
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestType_Equal(t *testing.T) {
+ t.Run("UnknownUnknown", func(t *testing.T) {
+ assert.True(t, Unknown.Equal(""))
+ })
+ t.Run("CubestripCubestrip", func(t *testing.T) {
+ assert.True(t, Cubestrip.Equal(Cubestrip.String()))
+ })
+ t.Run("CubestripCylindrical", func(t *testing.T) {
+ assert.False(t, Cubestrip.Equal(Cylindrical.String()))
+ })
+ t.Run("CylindricalUnknown", func(t *testing.T) {
+ assert.False(t, Cylindrical.Equal(Unknown.String()))
+ })
+}
+
+func TestType_NotEqual(t *testing.T) {
+ t.Run("UnknownUnknown", func(t *testing.T) {
+ assert.False(t, Unknown.NotEqual(""))
+ })
+ t.Run("CubestripCubestrip", func(t *testing.T) {
+ assert.False(t, Cubestrip.NotEqual(Cubestrip.String()))
+ })
+ t.Run("CubestripCylindrical", func(t *testing.T) {
+ assert.True(t, Cubestrip.NotEqual(Cylindrical.String()))
+ })
+ t.Run("CylindricalUnknown", func(t *testing.T) {
+ assert.True(t, Cylindrical.NotEqual(Unknown.String()))
+ })
+}
diff --git a/pkg/projection/types.go b/pkg/projection/types.go
new file mode 100644
index 000000000..c628ac206
--- /dev/null
+++ b/pkg/projection/types.go
@@ -0,0 +1,24 @@
+package projection
+
+const (
+ Unknown Type = ""
+ Equirectangular Type = "equirectangular"
+ Cubestrip Type = "cubestrip"
+ Cylindrical Type = "cylindrical"
+ TransverseCylindrical Type = "transverse-cylindrical"
+ PseudocylindricalCompromise Type = "pseudocylindrical-compromise"
+ Other Type = "other"
+)
+
+// Types maps identifiers to known types.
+var Types = Known{
+ string(Unknown): Unknown,
+ string(Equirectangular): Equirectangular,
+ string(Cubestrip): Cubestrip,
+ string(Cylindrical): Cylindrical,
+ string(TransverseCylindrical): TransverseCylindrical,
+ string(PseudocylindricalCompromise): PseudocylindricalCompromise,
+}
+
+// Known maps names to standard projection types.
+type Known map[string]Type
diff --git a/pkg/report/table.go b/pkg/report/table.go
index e0858b918..be341253f 100644
--- a/pkg/report/table.go
+++ b/pkg/report/table.go
@@ -2,6 +2,7 @@ package report
import (
"bytes"
+ "strings"
"github.com/olekukonko/tablewriter"
)
@@ -15,6 +16,17 @@ func Table(rows [][]string, cols []string, markDown bool) string {
// TableWithCaption returns a text-formatted table with caption, optionally as valid Markdown,
// so the output can be pasted into the docs.
func TableWithCaption(rows [][]string, cols []string, caption string, markDown bool) string {
+ // Escape Markdown.
+ if markDown {
+ for i := range rows {
+ for j := range rows[i] {
+ if strings.ContainsRune(rows[i][j], '|') {
+ rows[i][j] = strings.ReplaceAll(rows[i][j], "|", "\\|")
+ }
+ }
+ }
+ }
+
buf := &bytes.Buffer{}
// Set Borders.
diff --git a/pkg/report/table_test.go b/pkg/report/table_test.go
new file mode 100644
index 000000000..27edd3a04
--- /dev/null
+++ b/pkg/report/table_test.go
@@ -0,0 +1,28 @@
+package report
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTable(t *testing.T) {
+ t.Run("Standard", func(t *testing.T) {
+ cols := []string{"Col1", "Col2"}
+ rows := [][]string{
+ {"foo", "bar" + strings.Repeat(", abc", 30)},
+ {"bar", "b & a | z"}}
+ result := Table(rows, cols, false)
+ assert.Contains(t, result, "| bar | b & a | z |")
+ })
+ t.Run("Markdown", func(t *testing.T) {
+ cols := []string{"Col1", "Col2"}
+ rows := [][]string{
+ {"foo", "bar" + strings.Repeat(", abc", 30)},
+ {"bar", "b & a | z"}}
+ result := Table(rows, cols, true)
+ // fmt.Println(result)
+ assert.Contains(t, result, "| bar | b & a \\| z")
+ })
+}
diff --git a/pkg/rnd/generate_passwd.go b/pkg/rnd/generate_passwd.go
new file mode 100644
index 000000000..230d00cb0
--- /dev/null
+++ b/pkg/rnd/generate_passwd.go
@@ -0,0 +1,6 @@
+package rnd
+
+// GeneratePasswd returns a random password with 8 characters as string.
+func GeneratePasswd() string {
+ return GenerateToken(8)
+}
diff --git a/pkg/rnd/token.go b/pkg/rnd/generate_token.go
similarity index 81%
rename from pkg/rnd/token.go
rename to pkg/rnd/generate_token.go
index ed274cf7b..ee58a9dd7 100644
--- a/pkg/rnd/token.go
+++ b/pkg/rnd/generate_token.go
@@ -7,8 +7,8 @@ import (
"strconv"
)
-// Token returns a random token with length of up to 10 characters.
-func Token(size uint) string {
+// GenerateToken returns a random token with length of up to 10 characters.
+func GenerateToken(size uint) string {
if size > 10 || size < 1 {
panic(fmt.Sprintf("size out of range: %d", size))
}
diff --git a/pkg/rnd/token_test.go b/pkg/rnd/generate_token_test.go
similarity index 81%
rename from pkg/rnd/token_test.go
rename to pkg/rnd/generate_token_test.go
index c9854cbd0..2ef921b49 100644
--- a/pkg/rnd/token_test.go
+++ b/pkg/rnd/generate_token_test.go
@@ -8,35 +8,35 @@ import (
func TestRandomToken(t *testing.T) {
t.Run("size 4", func(t *testing.T) {
- token := Token(4)
+ token := GenerateToken(4)
assert.NotEmpty(t, token)
})
t.Run("size 8", func(t *testing.T) {
- token := Token(9)
+ token := GenerateToken(9)
assert.NotEmpty(t, token)
})
}
func TestRandomPassword(t *testing.T) {
- pw := Password()
+ pw := GeneratePasswd()
t.Logf("password: %s", pw)
assert.Equal(t, 8, len(pw))
}
func BenchmarkRandomPassword(b *testing.B) {
for n := 0; n < b.N; n++ {
- Password()
+ GeneratePasswd()
}
}
func BenchmarkRandomToken4(b *testing.B) {
for n := 0; n < b.N; n++ {
- Token(4)
+ GenerateToken(4)
}
}
func BenchmarkRandomToken3(b *testing.B) {
for n := 0; n < b.N; n++ {
- Token(3)
+ GenerateToken(3)
}
}
diff --git a/pkg/rnd/generate_uid.go b/pkg/rnd/generate_uid.go
new file mode 100644
index 000000000..1f9e133c6
--- /dev/null
+++ b/pkg/rnd/generate_uid.go
@@ -0,0 +1,16 @@
+package rnd
+
+import (
+ "strconv"
+ "time"
+)
+
+// GenerateUID returns a unique id with prefix as string.
+func GenerateUID(prefix byte) string {
+ result := make([]byte, 0, 16)
+ result = append(result, prefix)
+ result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
+ result = append(result, GenerateToken(9)...)
+
+ return string(result)
+}
diff --git a/pkg/rnd/generate_uid_test.go b/pkg/rnd/generate_uid_test.go
new file mode 100644
index 000000000..66ad16eb0
--- /dev/null
+++ b/pkg/rnd/generate_uid_test.go
@@ -0,0 +1,71 @@
+package rnd
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPPID(t *testing.T) {
+ for n := 0; n < 5; n++ {
+ uid := GenerateUID('x')
+ t.Logf("id: %s", uid)
+ assert.Equal(t, len(uid), 16)
+ }
+}
+
+func BenchmarkPPID(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ GenerateUID('x')
+ }
+}
+
+func TestIsPPID(t *testing.T) {
+ prefix := byte('x')
+
+ for n := 0; n < 10; n++ {
+ id := GenerateUID(prefix)
+ assert.True(t, EntityUID(id, prefix))
+ }
+
+ assert.True(t, EntityUID("lt9k3pw1wowuy3c2", 'l'))
+ assert.False(t, EntityUID("lt9k3pw1wowuy3c2123", 'l'))
+ assert.False(t, EntityUID("lt9k3pw1wowuy3c2123", 'l'))
+ assert.False(t, EntityUID("lt9k3pw1AAA-owuy3c2123", 'l'))
+ assert.False(t, EntityUID("", 'l'))
+ assert.False(t, EntityUID("lt9k3pw1w ?owuy 3c2123", 'l'))
+}
+
+func TestIsHex(t *testing.T) {
+ assert.True(t, IsHex("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
+ assert.True(t, IsHex("6ba7b810-9dad-11d1-80b4"))
+ assert.False(t, IsHex("55785BAC-9A4B-4747-B090-GE123FFEE437"))
+ assert.False(t, IsHex("550e8400-e29b-11d4-a716_446655440000"))
+ assert.True(t, IsHex("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
+ assert.False(t, IsHex(""))
+}
+
+func TestUniqueID(t *testing.T) {
+ assert.True(t, ValidID("lt9k3pw1wowuy3c2", 'l'))
+ assert.True(t, ValidID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb", 'l'))
+ assert.True(t, ValidID("6ba7b810-9dad-11d1-80b4-00c04fd430c8", 'l'))
+ assert.True(t, ValidID("55785BAC-9A4B-4747-B090-EE123FFEE437", 'l'))
+ assert.True(t, ValidID("550e8400-e29b-11d4-a716-446655440000", 'l'))
+ assert.False(t, ValidID("4B1FEF2D1CF4A5BE38B263E0637EDEAD", 'l'))
+ assert.False(t, ValidID("123", '1'))
+ assert.False(t, ValidID("_", '_'))
+ assert.False(t, ValidID("", '_'))
+}
+
+func TestUniqueIDs(t *testing.T) {
+ assert.True(t, ValidIDs([]string{"lt9k3pw1wowuy3c2", "ltxk3pwawowuy0c0"}, 'l'))
+ assert.True(t, ValidIDs([]string{"dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"}, 'l'))
+ assert.False(t, ValidIDs([]string{"_"}, '_'))
+ assert.False(t, ValidIDs([]string{""}, '_'))
+}
+
+func TestIsLowerAlnum(t *testing.T) {
+ assert.False(t, IsAlnum("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
+ assert.True(t, IsAlnum("dafbe7996a701cdb"))
+ assert.False(t, IsAlnum(""))
+}
diff --git a/pkg/rnd/generate_uuid.go b/pkg/rnd/generate_uuid.go
new file mode 100644
index 000000000..b03707502
--- /dev/null
+++ b/pkg/rnd/generate_uuid.go
@@ -0,0 +1,10 @@
+package rnd
+
+import (
+ uuid "github.com/satori/go.uuid"
+)
+
+// UUID returns a standard, random UUID as string.
+func UUID() string {
+ return uuid.NewV4().String()
+}
diff --git a/pkg/rnd/generate_uuid_test.go b/pkg/rnd/generate_uuid_test.go
new file mode 100644
index 000000000..cb7af43fb
--- /dev/null
+++ b/pkg/rnd/generate_uuid_test.go
@@ -0,0 +1,21 @@
+package rnd
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestUUID(t *testing.T) {
+ for n := 0; n < 5; n++ {
+ uuid := UUID()
+ t.Logf("token: %s", uuid)
+ assert.Equal(t, 36, len(uuid))
+ }
+}
+
+func BenchmarkUUID(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ UUID()
+ }
+}
diff --git a/pkg/rnd/password.go b/pkg/rnd/password.go
deleted file mode 100644
index 4965bfc6f..000000000
--- a/pkg/rnd/password.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package rnd
-
-// Password returns a random password with 8 characters as string.
-func Password() string {
- return Token(8)
-}
diff --git a/pkg/rnd/uid.go b/pkg/rnd/uid.go
deleted file mode 100644
index ad881d48a..000000000
--- a/pkg/rnd/uid.go
+++ /dev/null
@@ -1,89 +0,0 @@
-package rnd
-
-import (
- "strconv"
- "time"
-)
-
-// PPID returns a unique id with prefix as string.
-func PPID(prefix byte) string {
- result := make([]byte, 0, 16)
- result = append(result, prefix)
- result = append(result, strconv.FormatInt(time.Now().UTC().Unix(), 36)[0:6]...)
- result = append(result, Token(9)...)
-
- return string(result)
-}
-
-// IsPPID returns true if string is a unique id as generated by PhotoPrism.
-func IsPPID(s string, prefix byte) bool {
- if len(s) != 16 {
- return false
- }
-
- if !IsLowerAlnum(s) {
- return false
- }
-
- return prefix == 0 || s[0] == prefix
-}
-
-// IsHex returns true if the string only contains hex numbers, dashes and letters without whitespace.
-func IsHex(s string) bool {
- if s == "" {
- return false
- }
-
- for _, r := range s {
- if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 70) && r != 45 {
- return false
- }
- }
-
- return true
-}
-
-// IsLowerAlnum returns true if the string only contains alphanumeric ascii chars without whitespace.
-func IsLowerAlnum(s string) bool {
- if s == "" {
- return false
- }
-
- for _, r := range s {
- if (r < 48 || r > 57) && (r < 97 || r > 122) {
- return false
- }
- }
-
- return true
-}
-
-// IsUID returns true if string is a seemingly unique id.
-func IsUID(s string, prefix byte) bool {
- // Regular UUID.
- if IsUUID(s) {
- return true
- }
-
- // Not a known UID format.
- if len(s) != 16 {
- return false
- }
-
- return IsPPID(s, prefix)
-}
-
-// ContainsUIDs tests if a slice of strings contains UIDs only.
-func ContainsUIDs(s []string, prefix byte) bool {
- if len(s) < 1 {
- return false
- }
-
- for _, id := range s {
- if !IsUID(id, prefix) {
- return false
- }
- }
-
- return true
-}
diff --git a/pkg/rnd/uid_test.go b/pkg/rnd/uid_test.go
deleted file mode 100644
index 40784fd9c..000000000
--- a/pkg/rnd/uid_test.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package rnd
-
-import (
- "testing"
-
- "github.com/stretchr/testify/assert"
-)
-
-func TestPPID(t *testing.T) {
- for n := 0; n < 5; n++ {
- uid := PPID('x')
- t.Logf("id: %s", uid)
- assert.Equal(t, len(uid), 16)
- }
-}
-
-func BenchmarkPPID(b *testing.B) {
- for n := 0; n < b.N; n++ {
- PPID('x')
- }
-}
-
-func TestIsPPID(t *testing.T) {
- prefix := byte('x')
-
- for n := 0; n < 10; n++ {
- id := PPID(prefix)
- assert.True(t, IsPPID(id, prefix))
- }
-
- assert.True(t, IsPPID("lt9k3pw1wowuy3c2", 'l'))
- assert.False(t, IsPPID("lt9k3pw1wowuy3c2123", 'l'))
- assert.False(t, IsPPID("lt9k3pw1wowuy3c2123", 'l'))
- assert.False(t, IsPPID("lt9k3pw1AAA-owuy3c2123", 'l'))
- assert.False(t, IsPPID("", 'l'))
- assert.False(t, IsPPID("lt9k3pw1w ?owuy 3c2123", 'l'))
-}
-
-func TestIsHex(t *testing.T) {
- assert.True(t, IsHex("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
- assert.True(t, IsHex("6ba7b810-9dad-11d1-80b4"))
- assert.False(t, IsHex("55785BAC-9A4B-4747-B090-GE123FFEE437"))
- assert.False(t, IsHex("550e8400-e29b-11d4-a716_446655440000"))
- assert.True(t, IsHex("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
- assert.False(t, IsHex(""))
-}
-
-func TestIsUID(t *testing.T) {
- assert.True(t, IsUID("lt9k3pw1wowuy3c2", 'l'))
- assert.True(t, IsUID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb", 'l'))
- assert.True(t, IsUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8", 'l'))
- assert.True(t, IsUID("55785BAC-9A4B-4747-B090-EE123FFEE437", 'l'))
- assert.True(t, IsUID("550e8400-e29b-11d4-a716-446655440000", 'l'))
- assert.False(t, IsUID("4B1FEF2D1CF4A5BE38B263E0637EDEAD", 'l'))
- assert.False(t, IsUID("123", '1'))
- assert.False(t, IsUID("_", '_'))
- assert.False(t, IsUID("", '_'))
-}
-
-func TestContainsUIDs(t *testing.T) {
- assert.True(t, ContainsUIDs([]string{"lt9k3pw1wowuy3c2", "ltxk3pwawowuy0c0"}, 'l'))
- assert.True(t, ContainsUIDs([]string{"dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"}, 'l'))
- assert.False(t, ContainsUIDs([]string{"_"}, '_'))
- assert.False(t, ContainsUIDs([]string{""}, '_'))
-}
-
-func TestIsLowerAlnum(t *testing.T) {
- assert.False(t, IsLowerAlnum("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
- assert.True(t, IsLowerAlnum("dafbe7996a701cdb"))
- assert.False(t, IsLowerAlnum(""))
-}
diff --git a/pkg/rnd/uuid.go b/pkg/rnd/uuid.go
deleted file mode 100644
index febb573dc..000000000
--- a/pkg/rnd/uuid.go
+++ /dev/null
@@ -1,36 +0,0 @@
-package rnd
-
-import (
- "strings"
-
- uuid "github.com/satori/go.uuid"
-)
-
-// UUID returns a standard, random UUID as string.
-func UUID() string {
- return uuid.NewV4().String()
-}
-
-// IsUUID tests if the string looks like a standard UUID.
-func IsUUID(s string) bool {
- return len(s) == 36 && IsHex(s)
-}
-
-// SanitizeUUID normalizes UUIDs found in XMP or Exif metadata.
-func SanitizeUUID(s string) string {
- if s == "" {
- return ""
- }
-
- s = strings.Replace(strings.TrimSpace(s), "\"", "", -1)
-
- if start := strings.LastIndex(s, ":"); start != -1 {
- s = s[start+1:]
- }
-
- if !IsUUID(s) {
- return ""
- }
-
- return strings.ToLower(s)
-}
diff --git a/pkg/rnd/validation.go b/pkg/rnd/validation.go
new file mode 100644
index 000000000..b49705591
--- /dev/null
+++ b/pkg/rnd/validation.go
@@ -0,0 +1,100 @@
+package rnd
+
+import "strings"
+
+// ValidID checks if the string is a valid unique ID.
+func ValidID(s string, prefix byte) bool {
+ // Regular UUID.
+ if ValidUUID(s) {
+ return true
+ }
+
+ // Not a known GenerateUID format.
+ if len(s) != 16 {
+ return false
+ }
+
+ return EntityUID(s, prefix)
+}
+
+// ValidIDs checks if a slice of strings contains ValidIDs only.
+func ValidIDs(s []string, prefix byte) bool {
+ if len(s) < 1 {
+ return false
+ }
+
+ for _, id := range s {
+ if !ValidID(id, prefix) {
+ return false
+ }
+ }
+
+ return true
+}
+
+// EntityUID returns true if string is a unique id as generated by PhotoPrism.
+func EntityUID(s string, prefix byte) bool {
+ if len(s) != 16 {
+ return false
+ }
+
+ if !IsAlnum(s) {
+ return false
+ }
+
+ return prefix == 0 || s[0] == prefix
+}
+
+// ValidUUID tests if the string looks like a standard UUID.
+func ValidUUID(s string) bool {
+ return len(s) == 36 && IsHex(s)
+}
+
+// SanitizeUUID normalizes UUIDs found in XMP or Exif metadata.
+func SanitizeUUID(s string) string {
+ if s == "" {
+ return ""
+ }
+
+ s = strings.Replace(strings.TrimSpace(s), "\"", "", -1)
+
+ if start := strings.LastIndex(s, ":"); start != -1 {
+ s = s[start+1:]
+ }
+
+ if !ValidUUID(s) {
+ return ""
+ }
+
+ return strings.ToLower(s)
+}
+
+// IsAlnum returns true if the string only contains alphanumeric ascii chars without whitespace.
+func IsAlnum(s string) bool {
+ if s == "" {
+ return false
+ }
+
+ for _, r := range s {
+ if (r < 48 || r > 57) && (r < 97 || r > 122) {
+ return false
+ }
+ }
+
+ return true
+}
+
+// IsHex returns true if the string only contains hex numbers, dashes and letters without whitespace.
+func IsHex(s string) bool {
+ if s == "" {
+ return false
+ }
+
+ for _, r := range s {
+ if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 70) && r != 45 {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/pkg/rnd/uuid_test.go b/pkg/rnd/validation_test.go
similarity index 62%
rename from pkg/rnd/uuid_test.go
rename to pkg/rnd/validation_test.go
index 106f9ac04..6423cebd1 100644
--- a/pkg/rnd/uuid_test.go
+++ b/pkg/rnd/validation_test.go
@@ -6,26 +6,12 @@ import (
"github.com/stretchr/testify/assert"
)
-func TestUUID(t *testing.T) {
- for n := 0; n < 5; n++ {
- uuid := UUID()
- t.Logf("token: %s", uuid)
- assert.Equal(t, 36, len(uuid))
- }
-}
-
-func BenchmarkUUID(b *testing.B) {
- for n := 0; n < b.N; n++ {
- UUID()
- }
-}
-
func TestIsUUID(t *testing.T) {
- assert.True(t, IsUUID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
- assert.True(t, IsUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
- assert.False(t, IsUUID("55785BAC-9H4B-4747-B090-EE123FFEE437"))
- assert.True(t, IsUUID("550e8400-e29b-11d4-a716-446655440000"))
- assert.False(t, IsUUID("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
+ assert.True(t, ValidUUID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
+ assert.True(t, ValidUUID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
+ assert.False(t, ValidUUID("55785BAC-9H4B-4747-B090-EE123FFEE437"))
+ assert.True(t, ValidUUID("550e8400-e29b-11d4-a716-446655440000"))
+ assert.False(t, ValidUUID("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
}
func TestSanitizeUUID(t *testing.T) {
diff --git a/pkg/txt/strings.go b/pkg/txt/compare.go
similarity index 100%
rename from pkg/txt/strings.go
rename to pkg/txt/compare.go
diff --git a/pkg/txt/strings_test.go b/pkg/txt/compare_test.go
similarity index 100%
rename from pkg/txt/strings_test.go
rename to pkg/txt/compare_test.go
diff --git a/pkg/txt/separator.go b/pkg/txt/separator.go
new file mode 100644
index 000000000..bf2961fb4
--- /dev/null
+++ b/pkg/txt/separator.go
@@ -0,0 +1,29 @@
+package txt
+
+import (
+ "unicode"
+)
+
+// isSeparator reports whether the rune could mark a word boundary.
+func isSeparator(r rune) bool {
+ // ASCII alphanumerics and underscore are not separators
+ if r <= 0x7F {
+ switch {
+ case '0' <= r && r <= '9':
+ return false
+ case 'a' <= r && r <= 'z':
+ return false
+ case 'A' <= r && r <= 'Z':
+ return false
+ case r == '_', r == '\'':
+ return false
+ }
+ return true
+ }
+ // Letters and digits are not separators
+ if unicode.IsLetter(r) || unicode.IsDigit(r) {
+ return false
+ }
+ // Otherwise, all we can do for now is treat spaces as separators.
+ return unicode.IsSpace(r)
+}
diff --git a/pkg/txt/capitalization.go b/pkg/txt/title.go
similarity index 56%
rename from pkg/txt/capitalization.go
rename to pkg/txt/title.go
index d73df6e79..f77430e40 100644
--- a/pkg/txt/capitalization.go
+++ b/pkg/txt/title.go
@@ -5,38 +5,6 @@ import (
"unicode"
)
-// isSeparator reports whether the rune could mark a word boundary.
-func isSeparator(r rune) bool {
- // ASCII alphanumerics and underscore are not separators
- if r <= 0x7F {
- switch {
- case '0' <= r && r <= '9':
- return false
- case 'a' <= r && r <= 'z':
- return false
- case 'A' <= r && r <= 'Z':
- return false
- case r == '_', r == '\'':
- return false
- }
- return true
- }
- // Letters and digits are not separators
- if unicode.IsLetter(r) || unicode.IsDigit(r) {
- return false
- }
- // Otherwise, all we can do for now is treat spaces as separators.
- return unicode.IsSpace(r)
-}
-
-// UcFirst returns the string with the first character converted to uppercase.
-func UcFirst(str string) string {
- for i, v := range str {
- return string(unicode.ToUpper(v)) + str[i+1:]
- }
- return ""
-}
-
// Title returns the string with the first characters of each word converted to uppercase.
func Title(s string) string {
s = strings.ReplaceAll(s, "_", " ")
diff --git a/pkg/txt/uppercase.go b/pkg/txt/uppercase.go
new file mode 100644
index 000000000..ff39a8975
--- /dev/null
+++ b/pkg/txt/uppercase.go
@@ -0,0 +1,13 @@
+package txt
+
+import (
+ "unicode"
+)
+
+// UpperFirst returns the string with the first character converted to uppercase.
+func UpperFirst(str string) string {
+ for i, v := range str {
+ return string(unicode.ToUpper(v)) + str[i+1:]
+ }
+ return ""
+}
diff --git a/pkg/txt/capitalization_test.go b/pkg/txt/uppercase_test.go
similarity index 96%
rename from pkg/txt/capitalization_test.go
rename to pkg/txt/uppercase_test.go
index 56a34db6a..6a738d39a 100644
--- a/pkg/txt/capitalization_test.go
+++ b/pkg/txt/uppercase_test.go
@@ -35,16 +35,16 @@ func TestIsSeparator(t *testing.T) {
func TestUcFirst(t *testing.T) {
t.Run("photo-lover", func(t *testing.T) {
- assert.Equal(t, "Photo-lover", UcFirst("photo-lover"))
+ assert.Equal(t, "Photo-lover", UpperFirst("photo-lover"))
})
t.Run("cat", func(t *testing.T) {
- assert.Equal(t, "Cat", UcFirst("Cat"))
+ assert.Equal(t, "Cat", UpperFirst("Cat"))
})
t.Run("KwaZulu natal", func(t *testing.T) {
assert.Equal(t, "KwaZulu Natal", Title("KwaZulu natal"))
})
t.Run("empty string", func(t *testing.T) {
- assert.Equal(t, "", UcFirst(""))
+ assert.Equal(t, "", UpperFirst(""))
})
}
diff --git a/pkg/video/codecs.go b/pkg/video/codecs.go
new file mode 100644
index 000000000..8aa818636
--- /dev/null
+++ b/pkg/video/codecs.go
@@ -0,0 +1,27 @@
+package video
+
+type Codec string
+
+const (
+ UnknownCodec Codec = ""
+ CodecAVC Codec = "avc1"
+ CodecHEVC Codec = "hvc1"
+ CodecVVC Codec = "vvc"
+ CodecAV1 Codec = "av01"
+)
+
+// Codecs maps identifiers to codecs.
+var Codecs = StandardCodecs{
+ "": UnknownCodec,
+ "avc": CodecAVC,
+ "avc1": CodecAVC,
+ "hvc1": CodecHEVC,
+ "hvc": CodecHEVC,
+ "hevc": CodecHEVC,
+ "vvc": CodecVVC,
+ "av1": CodecAV1,
+ "av01": CodecAV1,
+}
+
+// StandardCodecs maps names to known codecs.
+type StandardCodecs map[string]Codec
diff --git a/pkg/video/codecs_test.go b/pkg/video/codecs_test.go
new file mode 100644
index 000000000..99807e929
--- /dev/null
+++ b/pkg/video/codecs_test.go
@@ -0,0 +1,17 @@
+package video
+
+import "testing"
+
+func TestCodecs(t *testing.T) {
+ if val := Codecs[""]; val != UnknownCodec {
+ t.Fatal("default codec should be UnknownCodec")
+ }
+
+ if val := Codecs["avc"]; val != CodecAVC {
+ t.Fatal("codec should be CodecAVC")
+ }
+
+ if val := Codecs["av1"]; val != CodecAV1 {
+ t.Fatal("codec should be CodecAV1")
+ }
+}
diff --git a/pkg/video/standards.go b/pkg/video/standards.go
new file mode 100644
index 000000000..00b718322
--- /dev/null
+++ b/pkg/video/standards.go
@@ -0,0 +1,20 @@
+package video
+
+// Types maps identifiers to standards.
+var Types = Standards{
+ "": AVC,
+ "mp4": MP4,
+ "mpeg4": MP4,
+ "avc": AVC,
+ "avc1": AVC,
+ "hvc": HEVC,
+ "hvc1": HEVC,
+ "hevc": HEVC,
+ "vvc": VVC,
+ "vvc1": VVC,
+ "av1": AV1,
+ "av01": AV1,
+}
+
+// Standards maps names to standardized formats.
+type Standards map[string]Type
diff --git a/pkg/video/type.go b/pkg/video/type.go
new file mode 100644
index 000000000..cb3f359cc
--- /dev/null
+++ b/pkg/video/type.go
@@ -0,0 +1,14 @@
+package video
+
+import (
+ "github.com/photoprism/photoprism/pkg/fs"
+)
+
+// Type represents a video format type.
+type Type struct {
+ File fs.Type
+ Codec Codec
+ Width int
+ Height int
+ Public bool
+}
diff --git a/pkg/video/types.go b/pkg/video/types.go
new file mode 100644
index 000000000..5edbc762d
--- /dev/null
+++ b/pkg/video/types.go
@@ -0,0 +1,50 @@
+package video
+
+import (
+ "github.com/photoprism/photoprism/pkg/fs"
+)
+
+// MP4 is a Multimedia Container (MPEG-4 Part 14).
+var MP4 = Type{
+ File: fs.VideoMP4,
+ Codec: CodecAVC,
+ Width: 0,
+ Height: 0,
+ Public: true,
+}
+
+// AVC aka Advanced Video Coding (H.264).
+var AVC = Type{
+ File: fs.VideoAVC,
+ Codec: CodecAVC,
+ Width: 0,
+ Height: 0,
+ Public: true,
+}
+
+// AV1 aka AOMedia Video 1.
+var AV1 = Type{
+ File: fs.VideoAV1,
+ Codec: CodecAV1,
+ Width: 0,
+ Height: 0,
+ Public: false,
+}
+
+// HEVC aka High Efficiency Video Coding (H.265).
+var HEVC = Type{
+ File: fs.VideoHEVC,
+ Codec: CodecHEVC,
+ Width: 0,
+ Height: 0,
+ Public: false,
+}
+
+// VVC aka Versatile Video Coding (H.266).
+var VVC = Type{
+ File: fs.VideoVVC,
+ Codec: CodecVVC,
+ Width: 0,
+ Height: 0,
+ Public: false,
+}
diff --git a/internal/video/formats_test.go b/pkg/video/types_test.go
similarity index 51%
rename from internal/video/formats_test.go
rename to pkg/video/types_test.go
index 42a22b40f..d5cbec2b6 100644
--- a/internal/video/formats_test.go
+++ b/pkg/video/types_test.go
@@ -2,16 +2,16 @@ package video
import "testing"
-func TestFormats(t *testing.T) {
- if val := Formats[""]; val != AVC {
+func TestTypes(t *testing.T) {
+ if val := Types[""]; val != AVC {
t.Fatal("default type should be avc")
}
- if val := Formats["mp4"]; val != MP4 {
+ if val := Types["mp4"]; val != MP4 {
t.Fatal("mp4 type should be mp4")
}
- if val := Formats["avc"]; val != AVC {
+ if val := Types["avc"]; val != AVC {
t.Fatal("mp4 type should be avc")
}
}
diff --git a/internal/video/video.go b/pkg/video/video.go
similarity index 100%
rename from internal/video/video.go
rename to pkg/video/video.go
|