From 8f9f74f913bb5bf727c0e72ad14301f544c6a0f6 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 28 Jan 2026 22:45:46 +0000 Subject: [PATCH 1/4] fix: correct tag caching key for folder-as-tags feature When using --folder-as-tags, assets in directories with identical names at different path levels were incorrectly assigned to the same tag. The root cause was that the tag cache was using Tag.Name (the leaf node name) as the cache key instead of Tag.Value (the full tag path). This caused collisions when multiple directories had the same name but different parent paths. Fixes #1262 --- app/upload/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/upload/run.go b/app/upload/run.go index d79961876..e62f2ff8b 100644 --- a/app/upload/run.go +++ b/app/upload/run.go @@ -557,7 +557,7 @@ func (uc *UpCmd) manageAssetTags(ctx context.Context, a *assets.Asset) { tags[i] = a.Tags[i].Name } for _, t := range a.Tags { - if uc.tagsCache.AddIDToCollection(t.Name, t, a.ID) { + if uc.tagsCache.AddIDToCollection(t.Value, t, a.ID) { // Record tag event uc.app.FileProcessor().Logger().Record(ctx, fileevent.ProcessedTagged, a.File, "tag", t.Value) } From 1cd59b7157a0bca45f5917d7c8b66639823927bb Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 29 Jan 2026 02:09:48 +0100 Subject: [PATCH 2/4] test: add folder structure with folder-as-tags test data --- .../folder-as-tags-test/2/same/telescopes_03.jpg | Bin 0 -> 6187 bytes .../2/unique2/telescopes_04.jpg | Bin 0 -> 5605 bytes .../one/same/telescopes_01.jpg | Bin 0 -> 7557 bytes .../one/unique1/telescopes_02.jpg | Bin 0 -> 1163 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 internal/e2e/client/DATA/fromFolder/folder-as-tags-test/2/same/telescopes_03.jpg create mode 100644 internal/e2e/client/DATA/fromFolder/folder-as-tags-test/2/unique2/telescopes_04.jpg create mode 100644 internal/e2e/client/DATA/fromFolder/folder-as-tags-test/one/same/telescopes_01.jpg create mode 100644 internal/e2e/client/DATA/fromFolder/folder-as-tags-test/one/unique1/telescopes_02.jpg diff --git a/internal/e2e/client/DATA/fromFolder/folder-as-tags-test/2/same/telescopes_03.jpg b/internal/e2e/client/DATA/fromFolder/folder-as-tags-test/2/same/telescopes_03.jpg new file mode 100644 index 0000000000000000000000000000000000000000..8471a6969eaf0b3ab55a0deb84dd85ef039e0a40 GIT binary patch literal 6187 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 literal 0 HcmV?d00001 diff --git a/internal/e2e/client/DATA/fromFolder/folder-as-tags-test/one/unique1/telescopes_02.jpg b/internal/e2e/client/DATA/fromFolder/folder-as-tags-test/one/unique1/telescopes_02.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5967d61d9f5bf091f572771785a1ab148617f2c7 GIT binary patch literal 1163 zcmex=0}~@NGZWC=K!*cW)&k`iSOi&x6b&8O zgaZ@Vl?p|S8YeE~Pwh=DOELf4NWZ*Q!{f5ODks=S2uSL zPp{yR(6I1`$f)F$)U@=B%&g*)(z5c3%Btp;*0%PJ&aO$5r%atTea6gLixw|gx@`H1 zm8&*w-m-Pu_8mKS9XfpE=&|D`PM*4S`O4L6*Kgds_3+W-Cr_U}fAR9w$4{TXeEs(Q z$IoAk49pPk07;04ko+SE^bZp-8d%st{$XS)2V!PH7FI<=HX+AA_QXPAC8I_T5vPd@ zHy-3vHV*nAnpAX=OH9S&q3TDFm%u(E&O=RP`3UYIxPSiNV&GwB1V$LMAcH-_FTHcO zG;*&vKNDcLxBs%HU#Q~!Av@_S3YWincW>J<{k=|>#F4$`6R-5%ssFX=+u{|sU(A!+ zcP)SBwM*yNX2q`Z7FfJM_T+;)(~r+qudP3n`qAifXtnsZ%X)b=KNn{=MR9DhIlGSG z*n9t7oaguEh6Q=wt?HT+Ts6N~b?KIECvAkE$~x;kY;u&8_gbsFe#3gUACEt_M||K} ze&`qPBxjjzA8gweobcA+6;N6Ce(DUv1GWrbO&?#Ym#fdjYs~rD#(%Y@KOLAxBXqPjcIQq0N-^A5e{JdgY zL{I7Q1ntvy+&|n8&r|rZ@!?tZ;LFo~`&`+=Htj950k{2hwdkTxYq*%N_gvq9`cvKU z9sX1552;wll=jbmmi@f{X56z!`rTF~fid1vRwrabPw?1mdu4su!`yDxN4=#F`o9L&(P@npP_TT Date: Thu, 29 Jan 2026 03:10:31 +0100 Subject: [PATCH 3/4] test: add e2e test for folder-as-tags with tag verification --- internal/e2e/client/fromFolder_test.go | 70 +++++++++++++++++++++++++- internal/e2e/e2eUtils/getAllTags.go | 42 ++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 internal/e2e/e2eUtils/getAllTags.go diff --git a/internal/e2e/client/fromFolder_test.go b/internal/e2e/client/fromFolder_test.go index 7382747db..640e40c95 100644 --- a/internal/e2e/client/fromFolder_test.go +++ b/internal/e2e/client/fromFolder_test.go @@ -133,4 +133,72 @@ func Test_FromFolder(t *testing.T) { fileevent.ProcessedTagged: 0, }, false, a.FileProcessor()) }) -} + t.Run("folder-as-tags", func(t *testing.T) { + adm, err := getUser("admin@immich.app") + if err != nil { + t.Fatalf("can't get admin user: %v", err) + } + // A fresh user for a new test + u1, err := createUser("minimal") + if err != nil { + t.Fatalf("can't create user: %v", err) + } + + ctx := t.Context() + c, a := root.RootImmichGoCommand(ctx) + c.SetArgs([]string{ + // "--concurrent-tasks=0", // for debugging + "upload", "from-folder", + "--server=" + ImmichURL, + "--api-key=" + u1.APIKey, + "--admin-api-key=" + adm.APIKey, + "--folder-as-tags=true", + "--no-ui", + "--api-trace", + "--log-level=debug", + "DATA/fromFolder/folder-as-tags-test", + }) + err = c.ExecuteContext(ctx) + if err != nil && a.Log().GetSLog() != nil { + a.Log().Error(err.Error()) + } + + if err != nil { + t.Error("Unexpected error", err) + return + } + + e2eutils.CheckResults(t, map[fileevent.Code]int64{ + fileevent.ProcessedUploadSuccess: 4, + fileevent.ProcessedAlbumAdded: 0, + fileevent.ProcessedTagged: 4, + }, false, a.FileProcessor()) + + // Verify that 4 different tags were created, not consolidated into fewer tags + // This is the critical check for issue #1262 + tags, err := e2eutils.GetAllTags(u1.Email, u1.Password) + if err != nil { + t.Fatalf("failed to get tags: %v", err) + } + + // Convert tag slice to map for faster lookup + tagMap := make(map[string]bool) + for _, tag := range tags { + tagMap[tag] = true + } + + // Verify each expected tag exists + expectedTags := []string{ + "folder-as-tags-test/one/same", + "folder-as-tags-test/one/unique1", + "folder-as-tags-test/2/same", + "folder-as-tags-test/2/unique2", + } + + for _, expectedTag := range expectedTags { + if !tagMap[expectedTag] { + t.Errorf("expected tag not found: %s", expectedTag) + } + } + }) +} \ No newline at end of file diff --git a/internal/e2e/e2eUtils/getAllTags.go b/internal/e2e/e2eUtils/getAllTags.go new file mode 100644 index 000000000..97027d5de --- /dev/null +++ b/internal/e2e/e2eUtils/getAllTags.go @@ -0,0 +1,42 @@ +package e2eutils + +import ( + "encoding/json" + "fmt" +) + +// TagSimplified represents a tag returned from the server +type TagSimplified struct { + ID string `json:"id"` + Name string `json:"name"` + Value string `json:"value"` +} + +// GetAllTags retrieves all tags for a user +// Returns a slice of tag values +func GetAllTags(email, password string) ([]string, error) { + // Login to get access token + token, err := UserLogin(email, password) + if err != nil { + return nil, fmt.Errorf("failed to login: %w", err) + } + + resp, err := do("GET", getAPIURL()+"/tags", make(map[string]string), token) + if err != nil { + return nil, fmt.Errorf("failed to get tags: %w", err) + } + defer resp.Body.Close() + + var tags []TagSimplified + err = json.NewDecoder(resp.Body).Decode(&tags) + if err != nil { + return nil, fmt.Errorf("failed to decode tags response: %w", err) + } + + tagValues := make([]string, len(tags)) + for i, tag := range tags { + tagValues[i] = tag.Value + } + + return tagValues, nil +} \ No newline at end of file From a8becacc8ee7182e7121480b8ef81f0ca65cbd28 Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 29 Jan 2026 21:19:30 +0100 Subject: [PATCH 4/4] test: exact comparison of assets and tags enhance folder-as-tags tests, checks tags on each file against the expected (instead of just checking if all tags exist) --- internal/e2e/client/fromFolder_test.go | 36 ++++--------- internal/e2e/e2eUtils/check.go | 35 ++++++++++++ internal/e2e/e2eUtils/client.go | 15 ++++-- internal/e2e/e2eUtils/getAllTags.go | 4 +- internal/e2e/e2eUtils/getAssets.go | 73 ++++++++++++++++++-------- 5 files changed, 110 insertions(+), 53 deletions(-) diff --git a/internal/e2e/client/fromFolder_test.go b/internal/e2e/client/fromFolder_test.go index 640e40c95..f913bd5c7 100644 --- a/internal/e2e/client/fromFolder_test.go +++ b/internal/e2e/client/fromFolder_test.go @@ -153,6 +153,7 @@ func Test_FromFolder(t *testing.T) { "--api-key=" + u1.APIKey, "--admin-api-key=" + adm.APIKey, "--folder-as-tags=true", + "--pause-immich-jobs=false", // has to scan sidecars to get tags immediately "--no-ui", "--api-trace", "--log-level=debug", @@ -174,31 +175,12 @@ func Test_FromFolder(t *testing.T) { fileevent.ProcessedTagged: 4, }, false, a.FileProcessor()) - // Verify that 4 different tags were created, not consolidated into fewer tags - // This is the critical check for issue #1262 - tags, err := e2eutils.GetAllTags(u1.Email, u1.Password) - if err != nil { - t.Fatalf("failed to get tags: %v", err) - } - - // Convert tag slice to map for faster lookup - tagMap := make(map[string]bool) - for _, tag := range tags { - tagMap[tag] = true - } - - // Verify each expected tag exists - expectedTags := []string{ - "folder-as-tags-test/one/same", - "folder-as-tags-test/one/unique1", - "folder-as-tags-test/2/same", - "folder-as-tags-test/2/unique2", - } - - for _, expectedTag := range expectedTags { - if !tagMap[expectedTag] { - t.Errorf("expected tag not found: %s", expectedTag) - } - } + // Map filenames to expected tags (derived from folder structure) + e2eutils.VerifyTagList(t, u1.Email, u1.Password, map[string][]string{ + "telescopes_01.jpg": {"folder-as-tags-test/one/same"}, + "telescopes_02.jpg": {"folder-as-tags-test/one/unique1"}, + "telescopes_03.jpg": {"folder-as-tags-test/2/same"}, + "telescopes_04.jpg": {"folder-as-tags-test/2/unique2"}, + }) }) -} \ No newline at end of file +} diff --git a/internal/e2e/e2eUtils/check.go b/internal/e2e/e2eUtils/check.go index 59e322f74..d5359396c 100644 --- a/internal/e2e/e2eUtils/check.go +++ b/internal/e2e/e2eUtils/check.go @@ -28,3 +28,38 @@ func CheckResults(t *testing.T, expectedResults map[fileevent.Code]int64, forced } return r } + +// VerifyTagList checks that all assets for the given user have the expected tags +func VerifyTagList(t *testing.T, email, password string, expectedTags map[string][]string) { + assets, err := GetAllAssets(email, password) + if err != nil { + t.Fatalf("failed to get assets: %v", err) + } + + for filename, expectedTagList := range expectedTags { + asset, exists := assets[filename] + if !exists { + t.Errorf("expected asset not found: %s", filename) + continue + } + asset, err := GetAssetDetails(email, password, asset.ID) + if err != nil { + t.Errorf("failed to get details for asset %s: %v", filename, err) + continue + } + + if len(asset.Tags) != len(expectedTagList) { + t.Errorf("asset %s: expected %d tags, got %d", filename, len(expectedTagList), len(asset.Tags)) + } + + TAGLIST: + for _, expectedTag := range expectedTagList { + for _, actualTag := range asset.Tags { + if actualTag.Value == expectedTag { + continue TAGLIST + } + } + t.Errorf("asset %s: expected tag not found: %s (got: %v)\n\n", filename, expectedTag, asset.Tags) + } + } +} diff --git a/internal/e2e/e2eUtils/client.go b/internal/e2e/e2eUtils/client.go index 4d050a950..b45c9c6f6 100644 --- a/internal/e2e/e2eUtils/client.go +++ b/internal/e2e/e2eUtils/client.go @@ -22,9 +22,14 @@ func getAPIURL() string { } func do(method string, url string, body any, token Token) (*http.Response, error) { - jsonBody, err := json.Marshal(body) - if err != nil { - return nil, fmt.Errorf("can't post %s: %w", url, err) + var jsonBody []byte + // Don't marshal nil into JSON "null" which some endpoints do not accept + if body != nil { + var err error + jsonBody, err = json.Marshal(body) + if err != nil { + return nil, fmt.Errorf("can't post %s: %w", url, err) + } } req, err := http.NewRequest(method, url, bytes.NewReader(jsonBody)) if err != nil { @@ -51,6 +56,10 @@ func do(method string, url string, body any, token Token) (*http.Response, error return resp, nil } +func get(url string, token Token) (*http.Response, error) { + return do(http.MethodGet, url, nil, token) +} + func post(url string, body any, token Token) (*http.Response, error) { return do(http.MethodPost, url, body, token) } diff --git a/internal/e2e/e2eUtils/getAllTags.go b/internal/e2e/e2eUtils/getAllTags.go index 97027d5de..ef4fcc1e4 100644 --- a/internal/e2e/e2eUtils/getAllTags.go +++ b/internal/e2e/e2eUtils/getAllTags.go @@ -21,7 +21,7 @@ func GetAllTags(email, password string) ([]string, error) { return nil, fmt.Errorf("failed to login: %w", err) } - resp, err := do("GET", getAPIURL()+"/tags", make(map[string]string), token) + resp, err := get(getAPIURL()+"/tags", token) if err != nil { return nil, fmt.Errorf("failed to get tags: %w", err) } @@ -39,4 +39,4 @@ func GetAllTags(email, password string) ([]string, error) { } return tagValues, nil -} \ No newline at end of file +} diff --git a/internal/e2e/e2eUtils/getAssets.go b/internal/e2e/e2eUtils/getAssets.go index b74c8b584..273b7b296 100644 --- a/internal/e2e/e2eUtils/getAssets.go +++ b/internal/e2e/e2eUtils/getAssets.go @@ -5,29 +5,37 @@ import ( "fmt" ) +type Tag struct { + ID string `json:"id"` + Name string `json:"name"` + Value string `json:"value"` + UpdatedAt string `json:"updatedAt"` +} + // Asset represents a simplified Immich asset returned from search +// update to include albums, exifInfo, owner, people if needed or use internal/assets type Asset struct { - ID string `json:"id"` - DeviceAssetID string `json:"deviceAssetId"` - DeviceID string `json:"deviceId"` - Type string `json:"type"` - OriginalPath string `json:"originalPath"` - OriginalFileName string `json:"originalFileName"` - Resized bool `json:"resized"` - Thumbhash string `json:"thumbhash"` - FileCreatedAt string `json:"fileCreatedAt"` - FileModifiedAt string `json:"fileModifiedAt"` - LocalDateTime string `json:"localDateTime"` - UpdatedAt string `json:"updatedAt"` - IsFavorite bool `json:"isFavorite"` - IsArchived bool `json:"isArchived"` - IsTrashed bool `json:"isTrashed"` - Duration string `json:"duration"` - Checksum string `json:"checksum"` - LivePhotoVideoID string `json:"livePhotoVideoId"` - Tags []string `json:"tags"` - Rating int `json:"rating"` - Visibility string `json:"visibility"` + ID string `json:"id"` + DeviceAssetID string `json:"deviceAssetId"` + DeviceID string `json:"deviceId"` + Type string `json:"type"` + OriginalPath string `json:"originalPath"` + OriginalFileName string `json:"originalFileName"` + Resized bool `json:"resized"` + Thumbhash string `json:"thumbhash"` + FileCreatedAt string `json:"fileCreatedAt"` + FileModifiedAt string `json:"fileModifiedAt"` + LocalDateTime string `json:"localDateTime"` + UpdatedAt string `json:"updatedAt"` + IsFavorite bool `json:"isFavorite"` + IsArchived bool `json:"isArchived"` + IsTrashed bool `json:"isTrashed"` + Duration string `json:"duration"` + Checksum string `json:"checksum"` + LivePhotoVideoID string `json:"livePhotoVideoId"` + Tags []Tag `json:"tags"` + Rating int `json:"rating"` + Visibility string `json:"visibility"` } // SearchMetadataRequest represents the request body for /search/metadata @@ -95,3 +103,26 @@ func GetAllAssets(email, password string) (map[string]*Asset, error) { return assetsByName, nil } + +// GetAssetDetails complete information about a specific asset, like its tags, albums +func GetAssetDetails(email, password, assetID string) (*Asset, error) { + // Login to get access token + token, err := UserLogin(email, password) + if err != nil { + return nil, fmt.Errorf("failed to login: %w", err) + } + + resp, err := get(getAPIURL()+"/assets/"+assetID, token) + if err != nil { + return nil, fmt.Errorf("failed to get asset details: %w", err) + } + defer resp.Body.Close() + + var asset Asset + err = json.NewDecoder(resp.Body).Decode(&asset) + if err != nil { + return nil, fmt.Errorf("failed to decode asset details response: %w", err) + } + + return &asset, nil +}