Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions cmd/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ func splitCommaList(raw string) []string {
return utils.RemoveEmptyStrings(values)
}

/**************************************************************************************************
** maskAPIKey renders an API key safe to log. Only the last four characters are kept, which is
** enough to tell several configured keys apart while keeping the secret out of the log file
** that LOG_FILE writes to disk.
**
** @param key - Raw API key
** @return string - Redacted form, e.g. "****cdef"
**************************************************************************************************/
func maskAPIKey(key string) string {
const visible = 4
runes := []rune(key)
if len(runes) <= visible {
return "****"
}
return "****" + string(runes[len(runes)-visible:])
}

/**************************************************************************************************
** filterOutPartnerAssets removes assets not owned by the current user from a fetched list
** and logs how many were dropped. Partner-shared assets surfaced by /search/metadata cannot
Expand Down
19 changes: 19 additions & 0 deletions cmd/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,22 @@ func TestSplitCommaList(t *testing.T) {
})
}
}

/**************************************************************************************************
** maskAPIKey keeps secrets out of the log file written by LOG_FILE.
**************************************************************************************************/
func TestMaskAPIKey(t *testing.T) {
cases := map[string]string{
"": "****",
"abc": "****",
"abcd": "****",
"abcde": "****bcde",
"super-secret-key": "****-key",
"clé-très-secrète": "****rète",
}
for input, expected := range cases {
if got := maskAPIKey(input); got != expected {
t.Errorf("maskAPIKey(%q) = %q, want %q", input, got, expected)
}
}
}
4 changes: 2 additions & 2 deletions cmd/duplicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ func runDuplicates(cmd *cobra.Command, args []string) {
}
client := immich.NewClient(apiURL, key, false, false, true, withArchived, withDeleted, false, includeVideos, stackConcurrency, nil, "", "", logger)
if client == nil {
logger.Errorf("Invalid client for API key: %s", key)
logger.Errorf("Invalid client for API key: %s", maskAPIKey(key))
continue
}
user, err := client.GetCurrentUser()
if err != nil {
logger.Errorf("Failed to fetch user for API key: %s: %v", key, err)
logger.Errorf("Failed to fetch user for API key: %s: %v", maskAPIKey(key), err)
continue
}
logger.Infof("=====================================================================================")
Expand Down
4 changes: 2 additions & 2 deletions cmd/fixtrash.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@ func runFixTrash(cmd *cobra.Command, args []string) {
func fixTrashForAPIKey(key string, logger *logrus.Logger) {
client := immich.NewClient(apiURL, key, false, false, dryRun, true, withDeleted, false, includeVideos, stackConcurrency, nil, "", "", logger)
if client == nil {
logger.Errorf("Invalid client for API key: %s", key)
logger.Errorf("Invalid client for API key: %s", maskAPIKey(key))
return
}
user, err := client.GetCurrentUser()
if err != nil {
logger.Errorf("Failed to fetch user for API key: %s: %v", key, err)
logger.Errorf("Failed to fetch user for API key: %s: %v", maskAPIKey(key), err)
return
}
logger.Infof("=====================================================================================")
Expand Down
29 changes: 19 additions & 10 deletions cmd/stacker.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,16 +128,23 @@ func needsStackUpdate(originalStack, expectedStack []string) bool {
** Identifies any child assets that are already part of existing stacks. This is used to
** prevent conflicts when creating new stacks and to handle stack replacement scenarios.
**
** Immich indexes every member asset of a stack to that same stack, so several children can carry
** the same stack ID. The IDs are deduplicated here: one DELETE per distinct stack, since every
** repeat would be answered with 400 "Not found or no stack.delete access" (issue #80).
**
** @param stack - Array of assets to check
** @return []string - Array of stack IDs where conflicts were found
** @return []string - Array of distinct stack IDs where conflicts were found
** @return bool - True if any conflicts were found
**************************************************************************************************/
func getChildrenWithStack(stack []utils.TAsset) ([]string, bool) {
childrenWithStack := make([]string, 0)
seenStackIDs := make(map[string]bool)
for _, asset := range stack[1:] {
if asset.Stack != nil {
childrenWithStack = append(childrenWithStack, asset.Stack.ID)
if asset.Stack == nil || seenStackIDs[asset.Stack.ID] {
continue
}
seenStackIDs[asset.Stack.ID] = true
childrenWithStack = append(childrenWithStack, asset.Stack.ID)
}
return childrenWithStack, len(childrenWithStack) > 0
}
Expand Down Expand Up @@ -182,12 +189,12 @@ func runStacker(cmd *cobra.Command, args []string) {
}
client := immich.NewClient(apiURL, key, resetStacks, replaceStacks, dryRun, withArchived, withDeleted, removeSingleAssetStacks, includeVideos, stackConcurrency, filterAlbumIDs, filterTakenAfter, filterTakenBefore, logger)
if client == nil {
logger.Errorf("Invalid client for API key: %s", key)
logger.Errorf("Invalid client for API key: %s", maskAPIKey(key))
continue
}
user, err := client.GetCurrentUser()
if err != nil {
logger.Errorf("Failed to fetch user for API key: %s: %v", key, err)
logger.Errorf("Failed to fetch user for API key: %s: %v", maskAPIKey(key), err)
continue
}
logger.Infof("=====================================================================================")
Expand Down Expand Up @@ -293,20 +300,22 @@ func processStack(client *immich.Client, logger *logrus.Logger, i int, total int
if replaceStacks {
for _, childID := range childrenWithStack {
msg, err := client.DeleteStackCollect(childID, utils.REASON_REPLACE_CHILD_STACK_WITH_NEW_ONE)
if err != nil {
if err != nil || msg == "" {
continue
}
deleteMsgs = append(deleteMsgs, msg)
}
}

/**********************************************************************************************
** Determine action type for logging.
** Determine action type for logging. The replace wording is driven by deleteMsgs, not by
** childrenWithStack: a child stack that was already gone, or whose delete failed, produces no
** message, and the report must not claim a deletion that never happened.
**********************************************************************************************/
var actionMsg string
if len(originalStackIDs) == 0 {
actionMsg = "\t🆕 Creating new stack"
} else if replaceStacks && len(childrenWithStack) > 0 {
} else if len(deleteMsgs) > 0 {
actionMsg = "\t🔄 Replacing existing stack (deleted child stacks)"
} else {
actionMsg = "\t✏️ Updating stack configuration"
Expand Down Expand Up @@ -384,12 +393,12 @@ func runCronLoopForAllUsers(apiKeys []string, apiURL string, logger *logrus.Logg
}
client := immich.NewClient(apiURL, key, resetStacks, replaceStacks, dryRun, withArchived, withDeleted, removeSingleAssetStacks, includeVideos, stackConcurrency, filterAlbumIDs, filterTakenAfter, filterTakenBefore, logger)
if client == nil {
logger.Errorf("Invalid client for API key: %s", key)
logger.Errorf("Invalid client for API key: %s", maskAPIKey(key))
continue
}
user, err := client.GetCurrentUser()
if err != nil {
logger.Errorf("Failed to fetch user for API key: %s: %v", key, err)
logger.Errorf("Failed to fetch user for API key: %s: %v", maskAPIKey(key), err)
continue
}
logger.Infof("=====================================================================================")
Expand Down
Loading
Loading