Improve UX and title generation from file names

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-06-29 11:10:24 +02:00
parent cf773b5714
commit bfd73932e5
24 changed files with 228 additions and 74 deletions

View file

@ -2252,9 +2252,9 @@
}
},
"@types/webpack": {
"version": "4.41.17",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.17.tgz",
"integrity": "sha512-6FfeCidTSHozwKI67gIVQQ5Mp0g4X96c2IXxX75hYEQJwST/i6NyZexP//zzMOBb+wG9jJ7oO8fk9yObP2HWAw==",
"version": "4.41.18",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.18.tgz",
"integrity": "sha512-mQm2R8vV2BZE/qIDVYqmBVLfX73a8muwjs74SpjEyJWJxeXBbsI9L65Pcia9XfYLYWzD1c1V8m+L0p30y2N7MA==",
"requires": {
"@types/anymatch": "*",
"@types/node": "*",
@ -3627,12 +3627,12 @@
}
},
"browserslist": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.1.tgz",
"integrity": "sha512-WMjXwFtPskSW1pQUDJRxvRKRkeCr7usN0O/Za76N+F4oadaTdQHotSGcX9jT/Hs7mSKPkyMFNvqawB/1HzYDKQ==",
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.2.tgz",
"integrity": "sha512-MfZaeYqR8StRZdstAK9hCKDd2StvePCYp5rHzQCPicUjfFliDgmuaBNPHYUTpAywBN8+Wc/d7NYVFkO0aqaBUw==",
"requires": {
"caniuse-lite": "^1.0.30001088",
"electron-to-chromium": "^1.3.481",
"electron-to-chromium": "^1.3.483",
"escalade": "^3.0.1",
"node-releases": "^1.1.58"
}
@ -3782,9 +3782,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001088",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001088.tgz",
"integrity": "sha512-6eYUrlShRYveyqKG58HcyOfPgh3zb2xqs7NvT2VVtP3hEUeeWvc3lqhpeMTxYWBBeeaT9A4bKsrtjATm66BTHg=="
"version": "1.0.30001090",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001090.tgz",
"integrity": "sha512-QzPRKDCyp7RhjczTPZaqK3CjPA5Ht2UnXhZhCI4f7QiB5JK6KEuZBxIzyWnB3wO4hgAj4GMRxAhuiacfw0Psjg=="
},
"center-align": {
"version": "0.1.3",
@ -5896,9 +5896,9 @@
}
},
"eslint-plugin-import": {
"version": "2.21.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz",
"integrity": "sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA==",
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz",
"integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==",
"requires": {
"array-includes": "^3.1.1",
"array.prototype.flat": "^1.2.3",
@ -9515,9 +9515,9 @@
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"nise": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/nise/-/nise-4.0.3.tgz",
"integrity": "sha512-EGlhjm7/4KvmmE6B/UFsKh7eHykRl9VH+au8dduHLCyWUO/hr7+N+WtTvDUwc9zHuM1IaIJs/0lQ6Ag1jDkQSg==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz",
"integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==",
"requires": {
"@sinonjs/commons": "^1.7.0",
"@sinonjs/fake-timers": "^6.0.0",

View file

@ -34,7 +34,7 @@
"babel-eslint": "^10.1.0",
"babel-loader": "^8.1.0",
"babel-plugin-istanbul": "^6.0.0",
"browserslist": "^4.12.1",
"browserslist": "^4.12.2",
"chai": "^4.2.0",
"chalk": "^4.1.0",
"chart.js": "^2.9.3",
@ -54,7 +54,7 @@
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^3.0.4",
"eslint-plugin-html": "^6.0.2",
"eslint-plugin-import": "^2.21.2",
"eslint-plugin-import": "^2.22.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",

View file

@ -87,7 +87,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile to="/review" @click="" v-if="$config.feature('review') && config.count.review > 0"
<v-list-tile to="/review" @click="" v-if="$config.feature('review')"
class="p-navigation-review">
<v-list-tile-content>
<v-list-tile-title>

View file

@ -230,6 +230,10 @@
if (this.results.length > 1) {
this.$notify.info(this.$gettext('All ') + this.results.length + this.$gettext(' entries loaded'));
}
} else if (this.results.length >= Photo.limit()) {
this.offset = offset;
this.scrollDisabled = true;
this.$notify.warn(this.$gettext("Can't load more, limit reached"));
} else {
this.offset = offset + count;
this.page++;

View file

@ -163,7 +163,7 @@
dirty: false,
results: [],
loading: true,
filesLimit: 500,
filesLimit: 1111,
filesOffset: 0,
page: 0,
selection: [],

View file

@ -271,6 +271,10 @@
if (this.results.length > 1) {
this.$notify.info(this.$gettext('All ') + this.results.length + this.$gettext(' photos loaded'));
}
} else if (this.results.length >= Photo.limit()) {
this.offset = offset;
this.scrollDisabled = true;
this.$notify.warn(this.$gettext("Can't load more, limit reached"));
} else {
this.offset = offset + count;
this.page++;

View file

@ -273,6 +273,10 @@
if (this.results.length > 1) {
this.$notify.info(this.$gettext('All ') + this.results.length + this.$gettext(' entries loaded'));
}
} else if (this.results.length >= Photo.limit()) {
this.offset = offset;
this.scrollDisabled = true;
this.$notify.warn(this.$gettext("Can't load more, limit reached"));
} else {
this.offset = offset + count;
this.page++;

2
go.mod
View file

@ -45,7 +45,7 @@ require (
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
golang.org/x/image v0.0.0-20200618115811-c13761719519 // indirect
golang.org/x/net v0.0.0-20200625001655-4c5254603344
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 // indirect
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae // indirect
google.golang.org/protobuf v1.25.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/ugjka/go-tz.v2 v2.0.9

2
go.sum
View file

@ -233,6 +233,8 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4 h1:5/PjkGUjvEU5Gl6BxmvKRPpqo2uNMv4rcHBMwzk/st8=
golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo=
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=

View file

@ -48,35 +48,31 @@ func wsReader(ws *websocket.Conn, writeMutex *sync.Mutex, connId string, conf *c
break
}
log.Debugf("websocket: received %d bytes", len(m))
var info clientInfo
if err := json.Unmarshal(m, &info); err != nil {
log.Error(err)
// Do nothing.
} else {
if sess := Session(info.SessionToken); sess.Valid() {
log.Debug("websocket: authenticated")
wsAuth.mutex.Lock()
wsAuth.user[connId] = sess.User
wsAuth.mutex.Unlock()
var clientConfig config.ClientConfig
if sess.User.Guest() {
clientConfig = conf.GuestConfig()
} else if sess.User.Registered() {
clientConfig = conf.UserConfig()
} else {
clientConfig = conf.PublicConfig()
}
writeMutex.Lock()
ws.SetWriteDeadline(time.Now().Add(30 * time.Second))
if sess.User.Guest() {
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.GuestConfig()}}); err != nil {
log.Error(err)
}
} else if sess.User.Registered() {
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.UserConfig()}}); err != nil {
log.Error(err)
}
} else {
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": conf.PublicConfig()}}); err != nil {
log.Error(err)
}
if err := ws.WriteJSON(gin.H{"event": "config.updated", "data": event.Data{"config": clientConfig}}); err != nil {
// Do nothing.
}
writeMutex.Unlock()
@ -117,13 +113,24 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
for {
select {
case <-pingTicker.C:
writeMutex.Lock()
ws.SetWriteDeadline(time.Now().Add(30 * time.Second))
if err := ws.WriteMessage(websocket.PingMessage, []byte{}); err != nil {
writeMutex.Unlock()
return
}
writeMutex.Unlock()
case msg := <-s.Receiver:
wsAuth.mutex.RLock()
user := wsAuth.user[connId]
user := entity.UnknownPerson
if hit, ok := wsAuth.user[connId]; ok {
user = hit
}
wsAuth.mutex.RUnlock()
if user.Registered() {
@ -132,9 +139,9 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
if err := ws.WriteJSON(gin.H{"event": msg.Name, "data": msg.Fields}); err != nil {
writeMutex.Unlock()
log.Debug(err)
return
}
writeMutex.Unlock()
}
}
@ -144,14 +151,12 @@ func wsWriter(ws *websocket.Conn, writeMutex *sync.Mutex, connId string) {
// GET /api/v1/ws
func Websocket(router *gin.RouterGroup) {
if router == nil {
log.Error("websocket: router is nil")
return
}
conf := service.Config()
if conf == nil {
log.Error("websocket: conf is nil")
return
}
@ -160,8 +165,8 @@ func Websocket(router *gin.RouterGroup) {
r := c.Request
ws, err := wsConnection.Upgrade(w, r, nil)
if err != nil {
log.Error(err)
return
}
@ -180,8 +185,6 @@ func Websocket(router *gin.RouterGroup) {
}
wsAuth.mutex.Unlock()
log.Debug("websocket: connected")
go wsWriter(ws, &writeMutex, connId)
wsReader(ws, &writeMutex, connId, conf)

View file

@ -116,7 +116,7 @@ func (c *Config) PublicConfig() ClientConfig {
return c.UserConfig()
}
defer log.Debug(capture.Time(time.Now(), "config: public config created"))
defer log.Debug(capture.Time(time.Now(), "client config created (public)"))
settings := c.Settings()
@ -155,7 +155,7 @@ func (c *Config) PublicConfig() ClientConfig {
// GuestConfig returns client config values for the sharing with guests.
func (c *Config) GuestConfig() ClientConfig {
defer log.Debug(capture.Time(time.Now(), "config: guest config created"))
defer log.Debug(capture.Time(time.Now(), "client config created (guest)"))
settings := c.Settings()
@ -196,7 +196,7 @@ func (c *Config) GuestConfig() ClientConfig {
// UserConfig returns client configuration values for registered users.
func (c *Config) UserConfig() ClientConfig {
defer log.Debug(capture.Time(time.Now(), "config: user config created"))
defer log.Debug(capture.Time(time.Now(), "client config created (user)"))
result := ClientConfig{
Settings: *c.Settings(),

View file

@ -535,13 +535,13 @@ func (m *Photo) DetailsLoaded() bool {
// TitleFromFileName returns a photo title based on the file name and/or path.
func (m *Photo) TitleFromFileName() string {
if fs.NonCanonical(m.PhotoName) {
if !fs.IsID(m.PhotoName) {
if title := txt.TitleFromFileName(m.PhotoName); title != "" {
return title
}
}
if m.OriginalName != "" && fs.NonCanonical(m.OriginalName) {
if m.OriginalName != "" && !fs.IsID(m.OriginalName) {
if title := txt.TitleFromFileName(m.OriginalName); title != "" {
return title
} else if title := txt.TitleFromFileName(path.Dir(m.OriginalName)); title != "" {

View file

@ -24,7 +24,7 @@ func (m *Photo) EstimateCountry() {
countryCode = code
}
if countryCode == unknown && fs.NonCanonical(m.PhotoName) {
if countryCode == unknown && !fs.IsID(m.PhotoName) {
if code := txt.CountryCode(m.PhotoName); code != unknown {
countryCode = code
} else if code := txt.CountryCode(m.PhotoPath); code != unknown {
@ -32,7 +32,7 @@ func (m *Photo) EstimateCountry() {
}
}
if countryCode == unknown && m.OriginalName != "" && fs.NonCanonical(m.OriginalName) {
if countryCode == unknown && m.OriginalName != "" && !fs.IsID(m.OriginalName) {
if code := txt.CountryCode(m.OriginalName); code != UnknownCountry.ID {
countryCode = code
}

View file

@ -1,11 +1,10 @@
package meta
import (
"regexp"
"strings"
)
var DscTitleRegexp = regexp.MustCompile("\\D{3}[\\d_]\\d{4}(.JPG)?")
"github.com/photoprism/photoprism/pkg/fs"
)
var UnwantedDescriptions = map[string]bool{
"OLYMPUS DIGITAL CAMERA": true,
@ -37,7 +36,7 @@ func SanitizeUID(value string) string {
func SanitizeTitle(value string) string {
value = SanitizeString(value)
if dsc := DscTitleRegexp.FindString(value); dsc == value {
if fs.IsID(value) {
value = ""
}

View file

@ -77,6 +77,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
fileChanged := true
fileExists := false
photoExists := false
stripSequence := Config().Settings().Index.Group
event.Publish("index.indexing", event.Data{
"fileHash": fileHash,
@ -138,7 +139,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
} else {
photo.PhotoQuality = -1
if yamlName := fs.TypeYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), Config().Settings().Index.Group); yamlName != "" {
if yamlName := fs.TypeYaml.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), stripSequence); yamlName != "" {
if err := photo.LoadFromYaml(yamlName); err != nil {
log.Errorf("index: %s (restore from yaml) for %s", err.Error(), quotedName)
} else {
@ -168,7 +169,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
file.OriginalName = originalName
if file.FilePrimary && photo.OriginalName == "" {
photo.OriginalName = originalName
photo.OriginalName = fs.Base(originalName, stripSequence)
}
}
@ -530,7 +531,7 @@ func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName string) (
w := txt.Keywords(photo.Details.Keywords)
if fs.NonCanonical(fileBase) {
if !fs.IsID(fileBase) {
w = append(w, txt.FilenameKeywords(filePath)...)
w = append(w, txt.FilenameKeywords(fileBase)...)
}

View file

@ -4,12 +4,15 @@ import (
"encoding/json"
"io/ioutil"
"path"
"sync"
"time"
gc "github.com/patrickmn/go-cache"
"github.com/photoprism/photoprism/internal/entity"
)
var fileMutex sync.RWMutex
// New returns a new session store with an optional cachePath.
func New(expiration time.Duration, cachePath string) *Session {
s := &Session{}
@ -17,6 +20,9 @@ func New(expiration time.Duration, cachePath string) *Session {
cleanupInterval := 15 * time.Minute
if cachePath != "" {
fileMutex.RLock()
defer fileMutex.RUnlock()
var savedItems map[string]Saved
items := make(map[string]gc.Item)
@ -70,6 +76,9 @@ func (s *Session) Save() error {
return nil
}
fileMutex.Lock()
defer fileMutex.Unlock()
items := s.cache.Items()
savedItems := make(map[string]Saved, len(items))

View file

@ -10,7 +10,7 @@ import (
func Base(fileName string, stripSequence bool) string {
basename := filepath.Base(fileName)
// strip file type extension
// Strip file type extension.
if end := strings.LastIndex(basename, "."); end != -1 {
basename = basename[:end]
}
@ -19,19 +19,19 @@ func Base(fileName string, stripSequence bool) string {
return basename
}
// strip numeric extensions like .0000, .0001, .4542353245,....
// Strip numeric extensions like .00000, .00001, .4542353245,.... (at least 5 digits).
if dot := strings.LastIndex(basename, "."); dot != -1 && len(basename[dot+1:]) >= 5 {
if i, err := strconv.Atoi(basename[dot+1:]); err == nil && i >= 0 {
basename = basename[:dot]
}
}
// other common sequential naming schemes
// Other common sequential naming schemes.
if end := strings.Index(basename, "("); end != -1 {
// copies created by Chrome & Windows, example: IMG_1234 (2)
// Copies created by Chrome & Windows, example: IMG_1234 (2).
basename = basename[:end]
} else if end := strings.Index(basename, " copy"); end != -1 {
// copies created by OS X, example: IMG_1234 copy 2
// Copies created by OS X, example: IMG_1234 copy 2.
basename = basename[:end]
}

View file

@ -5,7 +5,7 @@ import (
"time"
)
// NonCanonical returns true if the file basename is not canonical.
// NonCanonical returns true if the file basename is NOT canonical.
func NonCanonical(basename string) bool {
if len(basename) != 24 {
return true
@ -22,10 +22,15 @@ func NonCanonical(basename string) bool {
return false
}
// IsCanonical returns true if the file basename is canonical.
func IsCanonical(basename string) bool {
return !NonCanonical(basename)
}
// CanonicalName returns a canonical name based on time and CRC32 checksum.
func CanonicalName(date time.Time, checksum string) string {
if len(checksum) != 8 {
checksum = "ERROR000"
checksum = "EEEEEEEE"
} else {
checksum = strings.ToUpper(checksum)
}

View file

@ -26,6 +26,6 @@ func TestCanonicalName(t *testing.T) {
date := time.Date(
2009, 11, 17, 20, 34, 58, 651387237, time.UTC)
assert.Equal(t, "20091117_203458_ERROR000", CanonicalName(date, "123"))
assert.Equal(t, "20091117_203458_EEEEEEEE", CanonicalName(date, "123"))
assert.Equal(t, "20091117_203458_12345678", CanonicalName(date, "12345678"))
}

View file

@ -9,10 +9,10 @@ import (
)
// Hash returns the SHA1 hash of a file as string.
func Hash(filename string) string {
func Hash(fileName string) string {
var result []byte
file, err := os.Open(filename)
file, err := os.Open(fileName)
if err != nil {
return ""
@ -30,10 +30,10 @@ func Hash(filename string) string {
}
// Checksum returns the CRC32 checksum of a file as string.
func Checksum(filename string) string {
func Checksum(fileName string) string {
var result []byte
file, err := os.Open(filename)
file, err := os.Open(fileName)
if err != nil {
return ""
@ -49,3 +49,23 @@ func Checksum(filename string) string {
return hex.EncodeToString(hash.Sum(result))
}
// IsHash tests if a string looks like a hash.
func IsHash(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 70) {
return false
}
}
switch len(s) {
case 8, 16, 32, 40, 56, 64, 80, 128, 256:
return true
}
return false
}

55
pkg/fs/id.go Normal file
View file

@ -0,0 +1,55 @@
package fs
import (
"regexp"
"github.com/photoprism/photoprism/pkg/rnd"
)
var DscNameRegexp = regexp.MustCompile("\\D{3}[\\d_]\\d{4}(.JPG)?")
// IsInt tests if the file base is an integer number.
func IsInt(base string) bool {
if base == "" {
return false
}
for _, r := range base {
if r < 48 || r > 57 {
return false
}
}
return true
}
// IsID tests if the file name looks like an automatically created identifier.
func IsID(fileName string) bool {
if fileName == "" {
return false
}
base := Base(fileName, false)
if IsHash(base) {
return true
}
if IsInt(base) {
return true
}
if dsc := DscNameRegexp.FindString(base); dsc == base {
return true
}
if rnd.IsUID(base, 0) {
return true
}
if IsCanonical(base) {
return true
}
return false
}

28
pkg/fs/id_test.go Normal file
View file

@ -0,0 +1,28 @@
package fs
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsID(t *testing.T) {
assert.True(t, IsID("lt9k3pw1wowuy3c2"))
assert.True(t, IsID("dafbfeb8-a129-4e7c-9cf0-e7996a701cdb"))
assert.True(t, IsID("6ba7b810-9dad-11d1-80b4-00c04fd430c8"))
assert.True(t, IsID("55785BAC-9A4B-4747-B090-EE123FFEE437"))
assert.True(t, IsID("550e8400-e29b-11d4-a716-446655440000"))
assert.True(t, IsID("IMG_0599.JPG"))
assert.True(t, IsID("DSC10599"))
assert.True(t, IsID("20091117_203458_ERROR000"))
assert.True(t, IsID("20091117_203458_12345678"))
assert.True(t, IsID("4B1FEF2D1CF4A5BE38B263E0637EDEAD"))
assert.True(t, IsID("123"))
assert.False(t, IsID("_"))
assert.False(t, IsID(""))
assert.False(t, IsID("20191117-153400-Central-Park-New-York-2019-3qy.mov"))
assert.True(t, IsID("e98eb86480a72bd585d228a709f0622f90e86cbc.jpg"))
assert.True(t, IsID("IMG_8115.jpg"))
assert.False(t, IsID("01 Introduction Businessmodel.pdf"))
assert.False(t, IsID("A regular file name with 121345678643 numbers"))
}

View file

@ -35,7 +35,7 @@ func IsHex(s string) bool {
}
for _, r := range s {
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 90) && r != 45 {
if (r < 48 || r > 57) && (r < 97 || r > 102) && (r < 65 || r > 70) && r != 45 {
return false
}
}
@ -58,10 +58,15 @@ func IsLowerAlnum(s string) bool {
return true
}
// Returns true if the string looks like a standard UUID.
func IsUUID(s string) bool {
return len(s) == 36 && IsHex(s)
}
// IsUID returns true if string is a seemingly unique id.
func IsUID(s string, prefix byte) bool {
// Regular UUID.
if len(s) == 36 && IsHex(s) {
if IsUUID(s) {
return true
}

View file

@ -31,9 +31,24 @@ func TestIsPPID(t *testing.T) {
assert.True(t, IsPPID("lt9k3pw1wowuy3c2", '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"))
}
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"))
}
func TestIsUID(t *testing.T) {
assert.True(t, IsUID("lt9k3pw1wowuy3c2", 'l'))
// xmp.iid:dafbfeb8-a129-4e7c-9cf0-e7996a701cdb
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'))