mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
Add /folders API to get directory lists for browsing #260
Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import RestModel from "model/rest";
|
||||
import Api from "../common/api";
|
||||
|
||||
class Account extends RestModel {
|
||||
export class Account extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
ID: 0,
|
||||
|
||||
@@ -2,7 +2,7 @@ import RestModel from "model/rest";
|
||||
import Api from "common/api";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
class Album extends RestModel {
|
||||
export class Album extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
ID: 0,
|
||||
|
||||
118
frontend/src/model/folder.js
Normal file
118
frontend/src/model/folder.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import RestModel from "model/rest";
|
||||
import Api from "common/api";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
export const FolderRootOriginals = "originals"
|
||||
export const FolderRootImport = "import"
|
||||
|
||||
export class Folder extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
Root: "",
|
||||
Path: "",
|
||||
PPID: "",
|
||||
Title: "",
|
||||
Description: "",
|
||||
Type: "",
|
||||
Order: "",
|
||||
Favorite: false,
|
||||
Ignore: false,
|
||||
Hidden: false,
|
||||
Watch: false,
|
||||
Links: [],
|
||||
CreatedAt: "",
|
||||
UpdatedAt: "",
|
||||
};
|
||||
}
|
||||
|
||||
getEntityName() {
|
||||
return this.Root + "/" + this.Path;
|
||||
}
|
||||
|
||||
getId() {
|
||||
return this.PPID;
|
||||
}
|
||||
|
||||
thumbnailUrl(type) {
|
||||
return "/api/v1/folder/" + this.getId() + "/thumbnail/" + type;
|
||||
}
|
||||
|
||||
getDateString() {
|
||||
return DateTime.fromISO(this.CreatedAt).toLocaleString(DateTime.DATETIME_MED);
|
||||
}
|
||||
|
||||
toggleLike() {
|
||||
this.Favorite = !this.Favorite;
|
||||
|
||||
if (this.Favorite) {
|
||||
return Api.post(this.getEntityResource() + "/like");
|
||||
} else {
|
||||
return Api.delete(this.getEntityResource() + "/like");
|
||||
}
|
||||
}
|
||||
|
||||
like() {
|
||||
this.Favorite = true;
|
||||
return Api.post(this.getEntityResource() + "/like");
|
||||
}
|
||||
|
||||
unlike() {
|
||||
this.Favorite = false;
|
||||
return Api.delete(this.getEntityResource() + "/like");
|
||||
}
|
||||
|
||||
static findAll(path) {
|
||||
return this.search(path, {recursive: true})
|
||||
}
|
||||
|
||||
static search(path, params) {
|
||||
const options = {
|
||||
params: params,
|
||||
};
|
||||
|
||||
if (!path || path[0] !== "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
return Api.get(this.getCollectionResource() + path, options).then((response) => {
|
||||
let count = response.data.length;
|
||||
let limit = 0;
|
||||
let offset = 0;
|
||||
|
||||
if (response.headers) {
|
||||
if (response.headers["x-count"]) {
|
||||
count = parseInt(response.headers["x-count"]);
|
||||
}
|
||||
|
||||
if (response.headers["x-limit"]) {
|
||||
limit = parseInt(response.headers["x-limit"]);
|
||||
}
|
||||
|
||||
if (response.headers["x-offset"]) {
|
||||
offset = parseInt(response.headers["x-offset"]);
|
||||
}
|
||||
}
|
||||
|
||||
response.models = [];
|
||||
response.count = count;
|
||||
response.limit = limit;
|
||||
response.offset = offset;
|
||||
|
||||
for (let i = 0; i < response.data.length; i++) {
|
||||
response.models.push(new this(response.data[i]));
|
||||
}
|
||||
|
||||
return Promise.resolve(response);
|
||||
});
|
||||
}
|
||||
|
||||
static getCollectionResource() {
|
||||
return "folders";
|
||||
}
|
||||
|
||||
static getModelName() {
|
||||
return "Folder";
|
||||
}
|
||||
}
|
||||
|
||||
export default Folder;
|
||||
@@ -2,7 +2,7 @@ import RestModel from "model/rest";
|
||||
import Api from "common/api";
|
||||
import {DateTime} from "luxon";
|
||||
|
||||
class Label extends RestModel {
|
||||
export class Label extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
ID: 0,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import RestModel from "model/rest";
|
||||
|
||||
class Link extends RestModel {
|
||||
export class Link extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
LinkToken: "",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class Model {
|
||||
export class Model {
|
||||
constructor(values) {
|
||||
this.__originalValues = {};
|
||||
|
||||
|
||||
@@ -3,14 +3,14 @@ import Api from "common/api";
|
||||
import {DateTime} from "luxon";
|
||||
import Util from "common/util";
|
||||
|
||||
const SrcManual = "manual";
|
||||
const CodecAvc1 = "avc1";
|
||||
const TypeMP4 = "mp4";
|
||||
const TypeJpeg = "jpg";
|
||||
const YearUnknown = -1;
|
||||
const MonthUnknown = -1;
|
||||
export const SrcManual = "manual";
|
||||
export const CodecAvc1 = "avc1";
|
||||
export const TypeMP4 = "mp4";
|
||||
export const TypeJpeg = "jpg";
|
||||
export const YearUnknown = -1;
|
||||
export const MonthUnknown = -1;
|
||||
|
||||
class Photo extends RestModel {
|
||||
export class Photo extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
ID: 0,
|
||||
|
||||
@@ -2,7 +2,7 @@ import Api from "common/api";
|
||||
import Form from "common/form";
|
||||
import Model from "./model";
|
||||
|
||||
class Rest extends Model {
|
||||
export class Rest extends Model {
|
||||
getId() {
|
||||
return this.ID;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Api from "common/api";
|
||||
import Model from "./model";
|
||||
|
||||
class Settings extends Model {
|
||||
export class Settings extends Model {
|
||||
changed(key) {
|
||||
return (this[key] !== this.__originalValues[key]);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Api from "../common/api";
|
||||
|
||||
const thumbs = window.clientConfig.thumbnails;
|
||||
|
||||
class Thumb extends Model {
|
||||
export class Thumb extends Model {
|
||||
getDefaults() {
|
||||
return {
|
||||
uuid: "",
|
||||
|
||||
@@ -2,7 +2,7 @@ import RestModel from "model/rest";
|
||||
import Form from "common/form";
|
||||
import Api from "common/api";
|
||||
|
||||
class User extends RestModel {
|
||||
export class User extends RestModel {
|
||||
getDefaults() {
|
||||
return {
|
||||
ID: 0,
|
||||
|
||||
@@ -98,11 +98,12 @@
|
||||
import Event from "pubsub-js";
|
||||
import Settings from "model/settings";
|
||||
import Util from "../../common/util";
|
||||
import {Folder, FolderRootImport} from "model/folder";
|
||||
|
||||
export default {
|
||||
name: 'p-tab-import',
|
||||
data() {
|
||||
const root = {"name": "All files in import folder", "path": "/"}
|
||||
const root = {"path": "/", "name": this.$gettext("All files in import folder")}
|
||||
|
||||
return {
|
||||
settings: new Settings(this.$config.settings()),
|
||||
@@ -195,19 +196,20 @@
|
||||
created() {
|
||||
this.subscriptionId = Event.subscribe('import', this.handleEvent);
|
||||
this.loading = true;
|
||||
Api.get('import').then((r) => {
|
||||
const subDirs = r.data.dirs ? r.data.dirs : [];
|
||||
|
||||
Folder.findAll(FolderRootImport).then((r) => {
|
||||
const folders = r.models ? r.models : [];
|
||||
const currentPath = this.settings.import.path;
|
||||
let found = currentPath === this.root.path;
|
||||
|
||||
this.dirs = [this.root];
|
||||
|
||||
for (let i = 0; i < subDirs.length; i++) {
|
||||
if(currentPath === subDirs[i]) {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
if (currentPath === folders[i].Path) {
|
||||
found = true;
|
||||
}
|
||||
|
||||
this.dirs.push({name: Util.truncate(subDirs[i], 100, "..."), path: subDirs[i]});
|
||||
this.dirs.push({path: folders[i].Path, name: "/" + Util.truncate(folders[i].Path, 100, "...")});
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
|
||||
@@ -75,7 +75,8 @@
|
||||
outline
|
||||
v-if="config.count.hidden > 0"
|
||||
>
|
||||
The index currently contains {{ config.count.hidden }} hidden files. Their format may not be supported,
|
||||
The index currently contains {{ config.count.hidden }} hidden files. Their format may not be
|
||||
supported,
|
||||
they haven't been converted to JPEG yet or there are duplicates.
|
||||
</v-alert>
|
||||
</v-container>
|
||||
@@ -90,11 +91,12 @@
|
||||
import Event from "pubsub-js";
|
||||
import Settings from "model/settings";
|
||||
import Util from "common/util";
|
||||
import { Folder, FolderRootOriginals } from "model/folder";
|
||||
|
||||
export default {
|
||||
name: 'p-tab-index',
|
||||
data() {
|
||||
const root = {"name": "All originals", "path": "/"}
|
||||
const root = {"path": "/", "name": this.$gettext("All originals")}
|
||||
|
||||
return {
|
||||
settings: new Settings(this.$config.settings()),
|
||||
@@ -203,19 +205,20 @@
|
||||
created() {
|
||||
this.subscriptionId = Event.subscribe('index', this.handleEvent);
|
||||
this.loading = true;
|
||||
Api.get('index').then((r) => {
|
||||
const subDirs = r.data.dirs ? r.data.dirs : [];
|
||||
|
||||
Folder.findAll(FolderRootOriginals).then((r) => {
|
||||
const folders = r.models ? r.models : [];
|
||||
const currentPath = this.settings.index.path;
|
||||
let found = currentPath === this.root.path;
|
||||
|
||||
this.dirs = [this.root];
|
||||
|
||||
for (let i = 0; i < subDirs.length; i++) {
|
||||
if(currentPath === subDirs[i]) {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
if (currentPath === folders[i].Path) {
|
||||
found = true;
|
||||
}
|
||||
|
||||
this.dirs.push({name: Util.truncate(subDirs[i], 100, "..."), path: subDirs[i]});
|
||||
this.dirs.push({path: folders[i].Path, name: "/" + Util.truncate(folders[i].Path, 100, "...")});
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
|
||||
@@ -14,8 +14,8 @@ func TestGetAccounts(t *testing.T) {
|
||||
GetAccounts(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/accounts?count=10")
|
||||
val := gjson.Get(r.Body.String(), "#(AccName=\"Test Account\").AccURL")
|
||||
len := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(1), len.Int())
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(1), count.Int())
|
||||
assert.Equal(t, "http://webdav-dummy/", val.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
@@ -51,8 +51,8 @@ func TestGetAccountDirs(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetAccountDirs(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/accounts/1000000/dirs")
|
||||
len := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(2), len.Int())
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(2), count.Int())
|
||||
val := gjson.Get(r.Body.String(), "0.abs")
|
||||
assert.Equal(t, "/Photos", val.String())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/tidwall/gjson"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -13,8 +14,8 @@ func TestGetAlbums(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetAlbums(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/albums?count=10")
|
||||
len := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(3), len.Int())
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(3), count.Int())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
|
||||
39
internal/api/folder.go
Normal file
39
internal/api/folder.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
)
|
||||
|
||||
// GetFolders is a reusable request handler for directory listings (GET /api/v1/folders/*).
|
||||
func GetFolders(router *gin.RouterGroup, conf *config.Config, root, pathName string) {
|
||||
router.GET("/folders/" + root, func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
recursive := c.Query("recursive") != ""
|
||||
|
||||
folders, err := entity.Folders(root, pathName, recursive)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("folder: %s", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, folders)
|
||||
})
|
||||
}
|
||||
|
||||
// GET /api/v1/folders/originals
|
||||
func GetFoldersOriginals(router *gin.RouterGroup, conf *config.Config) {
|
||||
GetFolders(router, conf, entity.FolderRootOriginals, conf.OriginalsPath())
|
||||
}
|
||||
|
||||
// GET /api/v1/folders/import
|
||||
func GetFoldersImport(router *gin.RouterGroup, conf *config.Config) {
|
||||
GetFolders(router, conf, entity.FolderRootImport, conf.ImportPath())
|
||||
}
|
||||
166
internal/api/folder_test.go
Normal file
166
internal/api/folder_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGetFoldersOriginals(t *testing.T) {
|
||||
t.Run("flat", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
expected, err := fs.Dirs(conf.OriginalsPath(), false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
GetFoldersOriginals(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/folders/originals")
|
||||
|
||||
var folders []entity.Folder
|
||||
err = json.Unmarshal(r.Body.Bytes(), &folders)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(folders) != len(expected){
|
||||
t.Fatalf("response contains %d folders", len(folders))
|
||||
}
|
||||
|
||||
if len(folders) == 0 {
|
||||
// There are no existing folders, that's ok.
|
||||
return
|
||||
}
|
||||
|
||||
for _, folder := range folders {
|
||||
assert.Equal(t, "", folder.FolderDescription)
|
||||
assert.Equal(t, entity.TypeDefault, folder.FolderType)
|
||||
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
|
||||
assert.Equal(t, entity.FolderRootOriginals, folder.Root)
|
||||
assert.Equal(t, "", folder.FolderUUID)
|
||||
assert.Equal(t, false, folder.FolderFavorite)
|
||||
assert.Equal(t, false, folder.FolderHidden)
|
||||
assert.Equal(t, false, folder.FolderIgnore)
|
||||
assert.Equal(t, false, folder.FolderWatch)
|
||||
}
|
||||
|
||||
// t.Logf("ORIGINALS: %+v", folders)
|
||||
})
|
||||
t.Run("recursive", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
expected, err := fs.Dirs(conf.OriginalsPath(), true)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
GetFoldersOriginals(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/folders/originals?recursive=true")
|
||||
var folders []entity.Folder
|
||||
err = json.Unmarshal(r.Body.Bytes(), &folders)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(folders) != len(expected){
|
||||
t.Fatalf("response contains %d folders", len(folders))
|
||||
}
|
||||
|
||||
for _, folder := range folders {
|
||||
assert.Equal(t, "", folder.FolderDescription)
|
||||
assert.Equal(t, entity.TypeDefault, folder.FolderType)
|
||||
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
|
||||
assert.Equal(t, entity.FolderRootOriginals, folder.Root)
|
||||
assert.Equal(t, "", folder.FolderUUID)
|
||||
assert.Equal(t, false, folder.FolderFavorite)
|
||||
assert.Equal(t, false, folder.FolderHidden)
|
||||
assert.Equal(t, false, folder.FolderIgnore)
|
||||
assert.Equal(t, false, folder.FolderWatch)
|
||||
}
|
||||
|
||||
// t.Logf("ORIGINALS RECURSIVE: %+v", folders)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetFoldersImport(t *testing.T) {
|
||||
t.Run("flat", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
expected, err := fs.Dirs(conf.ImportPath(), false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
GetFoldersImport(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/folders/import")
|
||||
var folders []entity.Folder
|
||||
err = json.Unmarshal(r.Body.Bytes(), &folders)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(folders) != len(expected){
|
||||
t.Fatalf("response contains %d folders", len(folders))
|
||||
}
|
||||
|
||||
// t.Logf("IMPORT FOLDERS: %+v", folders)
|
||||
|
||||
if len(folders) == 0 {
|
||||
// There are no existing folders, that's ok.
|
||||
return
|
||||
}
|
||||
|
||||
for _, folder := range folders {
|
||||
assert.Equal(t, "", folder.FolderDescription)
|
||||
assert.Equal(t, entity.TypeDefault, folder.FolderType)
|
||||
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
|
||||
assert.Equal(t, entity.FolderRootImport, folder.Root)
|
||||
assert.Equal(t, "", folder.FolderUUID)
|
||||
assert.Equal(t, false, folder.FolderFavorite)
|
||||
assert.Equal(t, false, folder.FolderHidden)
|
||||
assert.Equal(t, false, folder.FolderIgnore)
|
||||
assert.Equal(t, false, folder.FolderWatch)
|
||||
}
|
||||
|
||||
})
|
||||
t.Run("recursive", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
expected, err := fs.Dirs(conf.ImportPath(), true)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
GetFoldersImport(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/folders/import?recursive=true")
|
||||
var folders []entity.Folder
|
||||
err = json.Unmarshal(r.Body.Bytes(), &folders)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(folders) != len(expected){
|
||||
t.Fatalf("response contains %d folders", len(folders))
|
||||
}
|
||||
|
||||
for _, folder := range folders {
|
||||
assert.Equal(t, "", folder.FolderDescription)
|
||||
assert.Equal(t, entity.TypeDefault, folder.FolderType)
|
||||
assert.Equal(t, entity.SortOrderName, folder.FolderOrder)
|
||||
assert.Equal(t, entity.FolderRootImport, folder.Root)
|
||||
assert.Equal(t, "", folder.FolderUUID)
|
||||
assert.Equal(t, false, folder.FolderFavorite)
|
||||
assert.Equal(t, false, folder.FolderHidden)
|
||||
assert.Equal(t, false, folder.FolderIgnore)
|
||||
assert.Equal(t, false, folder.FolderWatch)
|
||||
}
|
||||
|
||||
// t.Logf("IMPORT FOLDERS RECURSIVE: %+v", folders)
|
||||
})
|
||||
}
|
||||
@@ -18,24 +18,6 @@ import (
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// GET /api/v1/import
|
||||
func GetImportOptions(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/import", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
dirs, err := fs.Dirs(conf.ImportPath(), true)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("import: %s", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"dirs": dirs})
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/import*
|
||||
func StartImport(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/import/*path", func(c *gin.Context) {
|
||||
@@ -63,12 +45,10 @@ func StartImport(router *gin.RouterGroup, conf *config.Config) {
|
||||
|
||||
if subPath = c.Param("path"); subPath != "" && subPath != "/" {
|
||||
subPath = strings.Replace(subPath, ".", "", -1)
|
||||
log.Debugf("import sub path from url: %s", subPath)
|
||||
path = path + subPath
|
||||
path = filepath.Join(path, subPath)
|
||||
} else if f.Path != "" {
|
||||
subPath = strings.Replace(f.Path, ".", "", -1)
|
||||
log.Debugf("import sub path from request: %s", subPath)
|
||||
path = path + subPath
|
||||
path = filepath.Join(path, subPath)
|
||||
}
|
||||
|
||||
path = filepath.Clean(path)
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetImportOptions(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetImportOptions(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/import")
|
||||
assert.True(t, gjson.Get(r.Body.String(), "dirs").IsArray())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
}
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func TestCancelImport(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
|
||||
@@ -12,28 +12,9 @@ import (
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
|
||||
// GET /api/v1/index
|
||||
func GetIndexingOptions(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.GET("/index", func(c *gin.Context) {
|
||||
if Unauthorized(c, conf) {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
dirs, err := fs.Dirs(conf.OriginalsPath(), true)
|
||||
|
||||
if err != nil {
|
||||
log.Errorf("index: %s", err)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"dirs": dirs})
|
||||
})
|
||||
}
|
||||
|
||||
// POST /api/v1/index
|
||||
func StartIndexing(router *gin.RouterGroup, conf *config.Config) {
|
||||
router.POST("/index", func(c *gin.Context) {
|
||||
|
||||
@@ -7,16 +7,6 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetIndexingOptions(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
GetIndexingOptions(router, conf)
|
||||
r := PerformRequest(app, "GET", "/api/v1/index")
|
||||
assert.True(t, gjson.Get(r.Body.String(), "dirs").IsArray())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCancelIndex(t *testing.T) {
|
||||
t.Run("successful request", func(t *testing.T) {
|
||||
app, router, conf := NewApiTest()
|
||||
|
||||
@@ -13,8 +13,8 @@ func TestGetLabels(t *testing.T) {
|
||||
app, router, ctx := NewApiTest()
|
||||
GetLabels(router, ctx)
|
||||
r := PerformRequest(app, "GET", "/api/v1/labels?count=15")
|
||||
len := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(4), len.Int())
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(4), count.Int())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
t.Run("invalid request", func(t *testing.T) {
|
||||
|
||||
@@ -14,8 +14,8 @@ func TestGetPhotos(t *testing.T) {
|
||||
|
||||
GetPhotos(router, ctx)
|
||||
r := PerformRequest(app, "GET", "/api/v1/photos?count=10")
|
||||
len := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(2), len.Int())
|
||||
count := gjson.Get(r.Body.String(), "#")
|
||||
assert.LessOrEqual(t, int64(2), count.Int())
|
||||
assert.Equal(t, http.StatusOK, r.Code)
|
||||
})
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ func CreateAccount(form form.Account) (model *Account, err error) {
|
||||
return model, err
|
||||
}
|
||||
|
||||
// Save updates the entity using form data and stores it in the database.
|
||||
// Saves the entity using form data and stores it in the database.
|
||||
func (m *Account) Save(form form.Account) error {
|
||||
db := Db()
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ type Album struct {
|
||||
DeletedAt *time.Time `sql:"index"`
|
||||
}
|
||||
|
||||
// BeforeCreate computes a random UUID when a new album is created in database
|
||||
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
|
||||
func (m *Album) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsPPID(m.AlbumUUID, 'a') {
|
||||
return nil
|
||||
@@ -72,7 +72,7 @@ func (m *Album) SetName(name string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Save updates the entity using form data and stores it in the database.
|
||||
// Saves the entity using form data and stores it in the database.
|
||||
func (m *Album) Save(f form.Album) error {
|
||||
if err := deepcopier.Copy(m).From(f); err != nil {
|
||||
return err
|
||||
|
||||
@@ -19,12 +19,14 @@ const (
|
||||
SortOrderOldest = "oldest"
|
||||
SortOrderImported = "imported"
|
||||
SortOrderSimilar = "similar"
|
||||
SortOrderName = "name"
|
||||
|
||||
// unknown values
|
||||
YearUnknown = -1
|
||||
MonthUnknown = -1
|
||||
TitleUnknown = "Unknown"
|
||||
|
||||
TypeDefault = ""
|
||||
TypeImage = "image"
|
||||
TypeLive = "live"
|
||||
TypeVideo = "video"
|
||||
|
||||
@@ -31,6 +31,7 @@ type Types map[string]interface{}
|
||||
var Entities = Types{
|
||||
"errors": &Error{},
|
||||
"accounts": &Account{},
|
||||
"folders": &Folder{},
|
||||
"files": &File{},
|
||||
"files_share": &FileShare{},
|
||||
"files_sync": &FileSync{},
|
||||
|
||||
@@ -75,7 +75,7 @@ func FirstFileByHash(fileHash string) (File, error) {
|
||||
return file, q.Error
|
||||
}
|
||||
|
||||
// BeforeCreate computes a random UUID when a new file is created in database
|
||||
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
|
||||
func (m *File) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsPPID(m.FileUUID, 'f') {
|
||||
return nil
|
||||
@@ -141,7 +141,7 @@ func (m *File) AllFilesMissing() bool {
|
||||
return count == 0
|
||||
}
|
||||
|
||||
// Save stored the file in the database using the default connection.
|
||||
// Saves the file in the database.
|
||||
func (m *File) Save() error {
|
||||
if m.PhotoID == 0 {
|
||||
return fmt.Errorf("file: photo id is empty (%s)", m.FileUUID)
|
||||
|
||||
130
internal/entity/folder.go
Normal file
130
internal/entity/folder.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/photoprism/photoprism/internal/form"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
"github.com/ulule/deepcopier"
|
||||
)
|
||||
|
||||
const (
|
||||
FolderRootUnknown = ""
|
||||
FolderRootOriginals = "originals"
|
||||
FolderRootImport = "import"
|
||||
)
|
||||
|
||||
// Folder represents a file system directory.
|
||||
type Folder struct {
|
||||
Root string `gorm:"type:varbinary(255);unique_index:idx_folders_root_path;" json:"Root" yaml:"Root"`
|
||||
Path string `gorm:"type:varbinary(1024);unique_index:idx_folders_root_path;" json:"Path" yaml:"Path"`
|
||||
FolderUUID string `gorm:"type:varbinary(36);primary_key;" json:"PPID,omitempty" yaml:"PPID,omitempty"`
|
||||
FolderTitle string `gorm:"type:varchar(255);" json:"Title" yaml:"Title,omitempty"`
|
||||
FolderDescription string `gorm:"type:text;" json:"Description,omitempty" yaml:"Description,omitempty"`
|
||||
FolderType string `gorm:"type:varbinary(16);" json:"Type" yaml:"Type,omitempty"`
|
||||
FolderOrder string `gorm:"type:varbinary(32);" json:"Order" yaml:"Order,omitempty"`
|
||||
FolderFavorite bool `json:"Favorite" yaml:"Favorite"`
|
||||
FolderIgnore bool `json:"Ignore" yaml:"Ignore"`
|
||||
FolderHidden bool `json:"Hidden" yaml:"Hidden"`
|
||||
FolderWatch bool `json:"Watch" yaml:"Watch"`
|
||||
Links []Link `gorm:"foreignkey:ShareUUID;association_foreignkey:FolderUUID" json:"-" yaml:"-"`
|
||||
CreatedAt time.Time `json:"-" yaml:"-"`
|
||||
UpdatedAt time.Time `json:"-" yaml:"-"`
|
||||
ModifiedAt *time.Time `json:"ModifiedAt,omitempty" yaml:"-"`
|
||||
DeletedAt *time.Time `sql:"index" json:"-"`
|
||||
}
|
||||
|
||||
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
|
||||
func (m *Folder) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsPPID(m.FolderUUID, 'd') {
|
||||
return nil
|
||||
}
|
||||
|
||||
return scope.SetColumn("FolderUUID", rnd.PPID('d'))
|
||||
}
|
||||
|
||||
// NewFolder creates a new file system directory entity.
|
||||
func NewFolder(root, pathName string, modTime *time.Time) Folder {
|
||||
now := time.Now().UTC()
|
||||
|
||||
pathName = strings.Trim(pathName, string(os.PathSeparator))
|
||||
|
||||
if pathName == "" {
|
||||
pathName = "/"
|
||||
}
|
||||
|
||||
result := Folder{
|
||||
Root: root,
|
||||
Path: pathName,
|
||||
FolderType: TypeDefault,
|
||||
FolderOrder: SortOrderName,
|
||||
ModifiedAt: modTime,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
result.SetTitleFromPath()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// SetTitleFromPath updates the title based on the path name (e.g. when displaying it as an album).
|
||||
func (m *Folder) SetTitleFromPath() {
|
||||
s := m.Path
|
||||
s = strings.TrimSpace(s)
|
||||
|
||||
if s == "" || s == "/" {
|
||||
s = m.Root
|
||||
} else {
|
||||
s = path.Base(s)
|
||||
}
|
||||
|
||||
if len(m.Path) >= 6 && txt.IsUInt(s) {
|
||||
if date := txt.Time(m.Path); !date.IsZero() {
|
||||
if date.Day() > 1 {
|
||||
m.FolderTitle = date.Format("January 2, 2006")
|
||||
} else {
|
||||
m.FolderTitle = date.Format("January 2006")
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s = strings.ReplaceAll(s, "_", " ")
|
||||
s = strings.ReplaceAll(s, "-", " ")
|
||||
s = strings.Title(s)
|
||||
|
||||
m.FolderTitle = txt.Clip(s, txt.ClipDefault)
|
||||
}
|
||||
|
||||
// Saves the entity using form data and stores it in the database.
|
||||
func (m *Folder) Save(f form.Folder) error {
|
||||
if err := deepcopier.Copy(m).From(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return Db().Save(m).Error
|
||||
}
|
||||
|
||||
// Returns a slice of folders in a given directory incl sub directories in recursive mode.
|
||||
func Folders(root, dir string, recursive bool) ([]Folder, error) {
|
||||
dirs, err := fs.Dirs(dir, recursive)
|
||||
|
||||
if err != nil {
|
||||
return []Folder{}, err
|
||||
}
|
||||
|
||||
folders := make([]Folder, len(dirs))
|
||||
|
||||
for i, p := range dirs {
|
||||
folders[i] = NewFolder(root, p, nil)
|
||||
}
|
||||
|
||||
return folders, nil
|
||||
}
|
||||
48
internal/entity/folder_test.go
Normal file
48
internal/entity/folder_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewFolder(t *testing.T) {
|
||||
t.Run("2020/05", func(t *testing.T) {
|
||||
folder := NewFolder(FolderRootOriginals, "2020/05", nil)
|
||||
assert.Equal(t, FolderRootOriginals, folder.Root)
|
||||
assert.Equal(t, "2020/05", folder.Path)
|
||||
assert.Equal(t, "May 2020", folder.FolderTitle)
|
||||
assert.Equal(t, "", folder.FolderDescription)
|
||||
assert.Equal(t, "", folder.FolderType)
|
||||
assert.Equal(t, SortOrderName, folder.FolderOrder)
|
||||
assert.Equal(t, "", folder.FolderUUID)
|
||||
assert.Equal(t, false, folder.FolderFavorite)
|
||||
assert.Equal(t, false, folder.FolderHidden)
|
||||
assert.Equal(t, false, folder.FolderIgnore)
|
||||
assert.Equal(t, false, folder.FolderWatch)
|
||||
})
|
||||
|
||||
t.Run("/2020/05/01/", func(t *testing.T) {
|
||||
folder := NewFolder(FolderRootOriginals, "/2020/05/01/", nil)
|
||||
assert.Equal(t, "2020/05/01", folder.Path)
|
||||
assert.Equal(t, "May 2020", folder.FolderTitle)
|
||||
})
|
||||
|
||||
t.Run("/2020/05/23/", func(t *testing.T) {
|
||||
folder := NewFolder(FolderRootImport, "/2020/05/23/", nil)
|
||||
assert.Equal(t, "2020/05/23", folder.Path)
|
||||
assert.Equal(t, "May 23, 2020", folder.FolderTitle)
|
||||
})
|
||||
|
||||
t.Run("/2020/05/23 Birthday", func(t *testing.T) {
|
||||
folder := NewFolder(FolderRootUnknown, "/2020/05/23 Birthday", nil)
|
||||
assert.Equal(t, "2020/05/23 Birthday", folder.Path)
|
||||
assert.Equal(t, "23 Birthday", folder.FolderTitle)
|
||||
})
|
||||
|
||||
t.Run("name empty", func(t *testing.T) {
|
||||
folder := NewFolder(FolderRootOriginals, "", nil)
|
||||
assert.Equal(t, "/", folder.Path)
|
||||
assert.Equal(t, "Originals", folder.FolderTitle)
|
||||
})
|
||||
}
|
||||
@@ -30,7 +30,7 @@ type Label struct {
|
||||
New bool `gorm:"-"`
|
||||
}
|
||||
|
||||
// BeforeCreate computes a random UUID when a new label is created in database
|
||||
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
|
||||
func (m *Label) BeforeCreate(scope *gorm.Scope) error {
|
||||
if rnd.IsPPID(m.LabelUUID, 'l') {
|
||||
return nil
|
||||
|
||||
@@ -20,7 +20,7 @@ type Link struct {
|
||||
DeletedAt *time.Time `deepcopier:"skip" sql:"index"`
|
||||
}
|
||||
|
||||
// BeforeCreate creates a new URL token when a new link is created.
|
||||
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
|
||||
func (m *Link) BeforeCreate(scope *gorm.Scope) error {
|
||||
if err := scope.SetColumn("LinkToken", rnd.Token(10)); err != nil {
|
||||
return err
|
||||
|
||||
@@ -18,11 +18,11 @@ import (
|
||||
// Photo represents a photo, all its properties, and link to all its images and sidecar files.
|
||||
type Photo struct {
|
||||
ID uint `gorm:"primary_key" yaml:"-"`
|
||||
PhotoUUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uuid;" yaml:"PhotoID"`
|
||||
PhotoType string `gorm:"type:varbinary(8);default:'image';" json:"PhotoType" yaml:"Type"`
|
||||
TakenAt time.Time `gorm:"type:datetime;index:idx_photos_taken_uuid;" json:"TakenAt" yaml:"Taken"`
|
||||
TakenAtLocal time.Time `gorm:"type:datetime;" yaml:"-"`
|
||||
TakenSrc string `gorm:"type:varbinary(8);" json:"TakenSrc" yaml:"TakenSrc,omitempty"`
|
||||
PhotoUUID string `gorm:"type:varbinary(36);unique_index;index:idx_photos_taken_uuid;" yaml:"PPID"`
|
||||
PhotoType string `gorm:"type:varbinary(8);default:'image';" json:"PhotoType" yaml:"Type"`
|
||||
PhotoTitle string `gorm:"type:varchar(255);" json:"PhotoTitle" yaml:"Title"`
|
||||
TitleSrc string `gorm:"type:varbinary(8);" json:"TitleSrc" yaml:"TitleSrc,omitempty"`
|
||||
PhotoDescription string `gorm:"type:text;" json:"PhotoDescription" yaml:"Description,omitempty"`
|
||||
@@ -67,7 +67,7 @@ type Photo struct {
|
||||
DeletedAt *time.Time `sql:"index" yaml:"Deleted,omitempty"`
|
||||
}
|
||||
|
||||
// SavePhotoForm updates a model using form data and persists it in the database.
|
||||
// SavePhotoForm saves a model in the database using form data.
|
||||
func SavePhotoForm(model Photo, form form.Photo, geoApi string) error {
|
||||
db := Db()
|
||||
locChanged := model.PhotoLat != form.PhotoLat || model.PhotoLng != form.PhotoLng
|
||||
@@ -178,7 +178,7 @@ func (m *Photo) ClassifyLabels() classify.Labels {
|
||||
return result
|
||||
}
|
||||
|
||||
// BeforeCreate computes a unique UUID, and set a default takenAt before indexing a new photo
|
||||
// BeforeCreate creates a random UUID if needed before inserting a new row to the database.
|
||||
func (m *Photo) BeforeCreate(scope *gorm.Scope) error {
|
||||
if m.TakenAt.IsZero() || m.TakenAtLocal.IsZero() {
|
||||
now := time.Now()
|
||||
|
||||
@@ -12,8 +12,8 @@ type PhotoAlbum struct {
|
||||
Hidden bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Photo *Photo
|
||||
Album *Album
|
||||
Photo *Photo `gorm:"PRELOAD:false"`
|
||||
Album *Album `gorm:"PRELOAD:true"`
|
||||
}
|
||||
|
||||
// TableName returns PhotoAlbum table identifier "photos_albums"
|
||||
|
||||
23
internal/form/folder.go
Normal file
23
internal/form/folder.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package form
|
||||
|
||||
import "github.com/ulule/deepcopier"
|
||||
|
||||
// Folder represents a file system directory edit form.
|
||||
type Folder struct {
|
||||
Root string `json:"Root"`
|
||||
Path string `json:"Path"`
|
||||
FolderType string `json:"Type"`
|
||||
FolderOrder string `json:"Order"`
|
||||
FolderTitle string `json:"Title"`
|
||||
FolderDescription string `json:"Description"`
|
||||
FolderFavorite bool `json:"Favorite"`
|
||||
FolderIgnore bool `json:"Ignore"`
|
||||
FolderHidden bool `json:"Hidden"`
|
||||
FolderWatch bool `json:"Watch"`
|
||||
}
|
||||
|
||||
func NewFolder(m interface{}) (f Folder, err error) {
|
||||
err = deepcopier.Copy(m).To(&f)
|
||||
|
||||
return f, err
|
||||
}
|
||||
@@ -54,11 +54,12 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
|
||||
api.DislikeLabel(v1, conf)
|
||||
api.LabelThumbnail(v1, conf)
|
||||
|
||||
api.GetFoldersOriginals(v1, conf)
|
||||
api.GetFoldersImport(v1, conf)
|
||||
|
||||
api.Upload(v1, conf)
|
||||
api.GetImportOptions(v1, conf)
|
||||
api.StartImport(v1, conf)
|
||||
api.CancelImport(v1, conf)
|
||||
api.GetIndexingOptions(v1, conf)
|
||||
api.StartIndexing(v1, conf)
|
||||
api.CancelIndexing(v1, conf)
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ package txt
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var DateRegexp = regexp.MustCompile("\\D\\d{4}[\\-_]\\d{2}[\\-_]\\d{2}\\D")
|
||||
var DateRegexp = regexp.MustCompile("\\D\\d{4}[\\-_]\\d{2}[\\-_]\\d{2,}")
|
||||
var DateCanonicalRegexp = regexp.MustCompile("\\D\\d{8}_\\d{6}_\\w+\\.")
|
||||
var DatePathRegexp = regexp.MustCompile("\\D\\d{4}\\/\\d{1,2}\\/?\\d{0,2}\\D")
|
||||
var DateTimeRegexp = regexp.MustCompile("\\D\\d{4}[\\-_]\\d{2}[\\-_]\\d{2}.{1,4}\\d{2}\\D\\d{2}\\D\\d{2}\\D")
|
||||
var DatePathRegexp = regexp.MustCompile("\\D\\d{4}\\/\\d{1,2}\\/?\\d*")
|
||||
var DateTimeRegexp = regexp.MustCompile("\\D\\d{4}[\\-_]\\d{2}[\\-_]\\d{2}.{1,4}\\d{2}\\D\\d{2}\\D\\d{2,}")
|
||||
var DateIntRegexp = regexp.MustCompile("\\d{1,4}")
|
||||
|
||||
var (
|
||||
@@ -38,6 +39,14 @@ func Time(s string) (result time.Time) {
|
||||
}
|
||||
}()
|
||||
|
||||
if len(s) < 6 {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s, "/"){
|
||||
s = "/" + s
|
||||
}
|
||||
|
||||
b := []byte(s)
|
||||
|
||||
if found := DateCanonicalRegexp.Find(b); len(found) > 0 { // Is it a canonical name like "20120727_093920_97425909.jpg"?
|
||||
@@ -154,3 +163,18 @@ func Int(s string) int {
|
||||
|
||||
return int(result)
|
||||
}
|
||||
|
||||
// IsUInt returns true if a string only contains an unsigned integer.
|
||||
func IsUInt(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range s {
|
||||
if r < 48 || r > 57 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -87,6 +87,12 @@ func TestTime(t *testing.T) {
|
||||
|
||||
t.Run("2019/05/21", func(t *testing.T) {
|
||||
result := Time("2019/05/21")
|
||||
assert.False(t, result.IsZero())
|
||||
assert.Equal(t, "2019-05-21 00:00:00 +0000 UTC", result.String())
|
||||
})
|
||||
|
||||
t.Run("2019/05/2145", func(t *testing.T) {
|
||||
result := Time("2019/05/2145")
|
||||
assert.True(t, result.IsZero())
|
||||
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
|
||||
})
|
||||
@@ -151,7 +157,6 @@ func TestTime(t *testing.T) {
|
||||
assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", result.String())
|
||||
})
|
||||
|
||||
|
||||
t.Run("545452019/1/3/foo.jpg", func(t *testing.T) {
|
||||
result := Time("/2019/1/3/foo.jpg")
|
||||
assert.False(t, result.IsZero())
|
||||
|
||||
Reference in New Issue
Block a user