diff --git a/.github/workflows/inttest.yml b/.github/workflows/inttest.yml index 820aa447..443f4a61 100644 --- a/.github/workflows/inttest.yml +++ b/.github/workflows/inttest.yml @@ -20,8 +20,8 @@ jobs: go-version-file: go.mod - name: Install archive tools run: | - sudo apt-get update - sudo apt-get install -y rar p7zip-full zip unzip + sudo apt update + sudo apt install -y rar p7zip-full zip unzip - name: Build unpackerr run: go build -o unpackerr . - name: Clone integration tests diff --git a/examples/docker-compose.yml b/examples/docker-compose.yml index ed80e86b..e26384f9 100644 --- a/examples/docker-compose.yml +++ b/examples/docker-compose.yml @@ -142,6 +142,7 @@ services: - UN_FOLDER_0_DISABLE_LOG=false - UN_FOLDER_0_MOVE_BACK=false - UN_FOLDER_0_EXTRACT_ISOS=false + - UN_FOLDER_0_PRESERVE_EXEC=false - UN_FOLDER_0_SKIP_EMPTY=false ## Hook Payload ## Webhooks @@ -174,4 +175,4 @@ services: - UN_CMDHOOK_0_EXCLUDE_1=lidarr - UN_CMDHOOK_0_TIMEOUT=10s -## => Content Auto Generated, 30 SEP 2026 07:06 UTC +## => Content Auto Generated, 04 OCT 2026 06:03 UTC diff --git a/examples/unpackerr.conf.example b/examples/unpackerr.conf.example index 8a9c96eb..48026144 100644 --- a/examples/unpackerr.conf.example +++ b/examples/unpackerr.conf.example @@ -96,9 +96,15 @@ remnant_action = "rename" ## Do not wrap the number in quotes. Raise this only if you have fast disks and CPU. parallel = 1 -## Use these configurations to control the file modes used for newly extracted -## files and folders. Recommend 0644/0755 or 0666/0777. +## Extracted files are created with this mode. The process umask still +## applies, so 0666 becomes 0644 under umask 0022. Use 0644, or 0660 or +## 0666 when another user must write the extracted files. Archive +## execute bits are kept only for a watched folder with preserve_exec +## enabled. file_mode = "0644" + +## Extracted directories are created with this mode. The process umask +## still applies. dir_mode = "0755" ## List of passwords to use for encrypted archives. Must be a list of strings. @@ -185,7 +191,7 @@ passwords = [] ## The following sections can be repeated if you have more than one Sonarr, ## ## Radarr, Lidarr, Readarr, Folder, Webhook, and/or Command Hook. ## ## Identify each instance with a short key: [sonarr.uhd], [folder.tv]. ## -## Changed in v1.0.0 (September 2026): map keys, not [[array]] list rows. ## +## Changed in v1.0.0 (October 2026): map keys, not [[array]] list rows. ## ## You MUST uncomment the [header.0], url and api_key for any Starr app. ## ## Uncomment [sonarr.0], [radarr.0], [lidarr.0], and/or [readarr.0] plus the ## ## url and api_key if that app is in use. ## @@ -443,6 +449,12 @@ passwords = [] # move_back = false ## Set this to true if you want this app to extract ISO files with .iso extension. # extract_isos = false +## When true, an execute bit stored in the archive is kept for owner, +## group, or other only when file_mode already grants that class access. +## A 6 beside an archived 7 becomes 7, a 4 becomes 5, and a 0 stays 0. +## Off, extracted files use file_mode only. Starr extracts stay off. +## The process umask still applies. +# preserve_exec = false ## Incomplete-download suffixes such as `.part` or `.crdownload`. Extraction ## stays in waiting as long as any matching file is in the watched item's ## top folder. Subfolders are not scanned. The UI and config always store @@ -547,4 +559,4 @@ passwords = [] ## You can adjust how long to wait for the command to run. # timeout = "10s" -## => Content Auto Generated, 30 SEP 2026 07:06 UTC +## => Content Auto Generated, 04 OCT 2026 06:03 UTC diff --git a/frontend/src/lib/i18n/locales/el.json b/frontend/src/lib/i18n/locales/el.json index 227d4e95..bfa49fd2 100644 --- a/frontend/src/lib/i18n/locales/el.json +++ b/frontend/src/lib/i18n/locales/el.json @@ -523,13 +523,13 @@ }, "fileMode": { "label": "Δικαιώματα αρχείων", - "description": "Δικαιώματα για νέα εξαγμένα αρχεία.", - "tooltip": "Μόνο Unix. Αγνοείται στα Windows." + "description": "Δικαιώματα για εξαγμένα αρχεία.", + "tooltip": "Μόνο Unix. Τα εξαγμένα αρχεία δημιουργούνται με αυτή τη λειτουργία. Η umask της διεργασίας εξακολουθεί να ισχύει. Αγνοείται στα Windows." }, "dirMode": { "label": "Δικαιώματα φακέλων", - "description": "Δικαιώματα για νέους εξαγμένους φακέλους.", - "tooltip": "Μόνο Unix. Αγνοείται στα Windows." + "description": "Δικαιώματα για εξαγμένους φακέλους.", + "tooltip": "Μόνο Unix. Οι εξαγμένοι φάκελοι δημιουργούνται με αυτή τη λειτουργία. Η umask της διεργασίας εξακολουθεί να ισχύει. Αγνοείται στα Windows." }, "logFile": { "label": "Αρχείο καταγραφής", @@ -797,6 +797,11 @@ "description": "Τα αρχεία .iso αντιμετωπίζονται ως αρχειοθήκες.", "tooltip": "Ανενεργό αγνοεί εικόνες ISO. Ενεργοποιήστε το μόνο αν θέλετε πραγματικά να αποσυμπιεστούν." }, + "preserve_exec": { + "label": "Διατήρηση bits εκτέλεσης", + "description": "Διατήρηση των bits εκτέλεσης του αρχείου.", + "tooltip": "Ανενεργό, τα εξαγμένα αρχεία παίρνουν μόνο τα δικαιώματα αρχείων. Ενεργό, ένα bit εκτέλεσης από το αρχείο διατηρείται για κάτοχο, ομάδα ή άλλους όταν τα δικαιώματα αρχείων δίνουν ήδη πρόσβαση σε αυτή την κλάση. Οι εξαγωγές Starr μένουν ανενεργές." + }, "disableRecursion": { "label": "Αναδρομή", "description": "Εξαγωγή αρχειοθηκών μέσα σε άλλες αρχειοθήκες.", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index b988fc52..08152a28 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -523,13 +523,13 @@ }, "fileMode": { "label": "File mode", - "description": "Permissions for newly extracted files.", - "tooltip": "Unix only. Ignored on Windows." + "description": "Mode for extracted files.", + "tooltip": "Unix only. Extracted files are created with this mode. Umask still applies. Ignored on Windows." }, "dirMode": { "label": "Dir mode", - "description": "Permissions for newly extracted folders.", - "tooltip": "Unix only. Ignored on Windows." + "description": "Mode for extracted folders.", + "tooltip": "Unix only. Extracted folders are created with this mode. Umask still applies. Ignored on Windows." }, "logFile": { "label": "Log file", @@ -797,6 +797,11 @@ "description": "Treat .iso files as archives.", "tooltip": "Off ignores ISO images. Turn this on only if you really want them unpacked." }, + "preserve_exec": { + "label": "Keep execute bits", + "description": "Keep execute bits stored in the archive.", + "tooltip": "Off, extracted files use File mode only. On, an archived execute bit is kept for owner, group, or other when File mode already grants that class access. Starr extracts stay off." + }, "disableRecursion": { "label": "Recursion", "description": "Extract archives found inside other archives.", diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index 62f2aec7..ff65f3a3 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -523,13 +523,13 @@ }, "fileMode": { "label": "Modo de archivo", - "description": "Permisos para archivos extraídos nuevos.", - "tooltip": "Solo Unix. Se ignora en Windows." + "description": "Modo de los archivos extraídos.", + "tooltip": "Solo Unix. Los archivos extraídos se crean con este modo. La umask del proceso sigue aplicando. Se ignora en Windows." }, "dirMode": { "label": "Modo de carpeta", - "description": "Permisos para carpetas extraídas nuevas.", - "tooltip": "Solo Unix. Se ignora en Windows." + "description": "Modo de las carpetas extraídas.", + "tooltip": "Solo Unix. Las carpetas extraídas se crean con este modo. La umask del proceso sigue aplicando. Se ignora en Windows." }, "logFile": { "label": "Archivo de registro", @@ -797,6 +797,11 @@ "description": "Tratar los archivos .iso como archivos comprimidos.", "tooltip": "Apagado ignora las imágenes ISO. Actívalo solo si de verdad quieres desempaquetarlas." }, + "preserve_exec": { + "label": "Conservar bits de ejecución", + "description": "Conservar los bits de ejecución del archivo.", + "tooltip": "Apagado, los archivos extraídos usan solo el modo de archivo. Encendido, un bit de ejecución del archivo se conserva para propietario, grupo u otros si el modo de archivo ya concede acceso a esa clase. Las extracciones de Starr siguen apagadas." + }, "disableRecursion": { "label": "Recursión", "description": "Extraer archivos encontrados dentro de otros archivos.", diff --git a/frontend/src/lib/i18n/locales/nl.json b/frontend/src/lib/i18n/locales/nl.json index 60634ccb..9f131cca 100644 --- a/frontend/src/lib/i18n/locales/nl.json +++ b/frontend/src/lib/i18n/locales/nl.json @@ -523,13 +523,13 @@ }, "fileMode": { "label": "Bestandsmodus", - "description": "Rechten voor nieuw uitgepakte bestanden.", - "tooltip": "Alleen Unix. Wordt op Windows genegeerd." + "description": "Modus voor uitgepakte bestanden.", + "tooltip": "Alleen Unix. Uitgepakte bestanden krijgen deze modus. De umask van het proces geldt nog. Wordt op Windows genegeerd." }, "dirMode": { "label": "Mapmodus", - "description": "Rechten voor nieuw uitgepakte mappen.", - "tooltip": "Alleen Unix. Wordt op Windows genegeerd." + "description": "Modus voor uitgepakte mappen.", + "tooltip": "Alleen Unix. Uitgepakte mappen krijgen deze modus. De umask van het proces geldt nog. Wordt op Windows genegeerd." }, "logFile": { "label": "Logbestand", @@ -797,6 +797,11 @@ "description": ".iso-bestanden als archieven behandelen.", "tooltip": "Uit negeert ISO-images. Zet dit alleen aan als je ze echt wilt uitpakken." }, + "preserve_exec": { + "label": "Uitvoerbits behouden", + "description": "Uitvoerbits uit het archief behouden.", + "tooltip": "Uit krijgen uitgepakte bestanden alleen de bestandsmodus. Aan blijft een uitvoerbit uit het archief staan voor eigenaar, groep of anderen als de bestandsmodus die klasse al toegang geeft. Starr-extracties blijven uit." + }, "disableRecursion": { "label": "Recursie", "description": "Archieven in andere archieven uitpakken.", diff --git a/frontend/src/lib/slug.ts b/frontend/src/lib/slug.ts index 01317452..060d8b23 100644 --- a/frontend/src/lib/slug.ts +++ b/frontend/src/lib/slug.ts @@ -192,6 +192,7 @@ export const FOLDER_ENV_FIELDS: Record = { move_back: 'MOVE_BACK', delete_after: 'DELETE_AFTER', extract_isos: 'EXTRACT_ISOS', + preserve_exec: 'PRESERVE_EXEC', disableRecursion: 'DISABLE_RECURSION', maxNested: 'MAX_NESTED', extrasMaxDepth: 'EXTRAS_MAX_DEPTH', diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index 659bf282..5c0a9bbd 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -294,6 +294,7 @@ export interface FolderConfig { move_back: boolean delete_after: string | null extract_isos: boolean + preserve_exec: boolean disableRecursion: boolean maxNested: number extrasMaxDepth: number diff --git a/frontend/src/pages/settings/FoldersForm.svelte b/frontend/src/pages/settings/FoldersForm.svelte index dfacdfbe..a5543723 100644 --- a/frontend/src/pages/settings/FoldersForm.svelte +++ b/frontend/src/pages/settings/FoldersForm.svelte @@ -80,6 +80,7 @@ move_back: false, delete_after: '', extract_isos: false, + preserve_exec: false, disableRecursion: false, maxNested: 0, extrasMaxDepth: 0, @@ -469,6 +470,18 @@ envVar={envField(envPrefix, slug, 'EXTRACT_ISOS')} /> + + + Folder Config: 1 path: %s%s; delete_after:%v delete_orig:%v delete_files:%v "+ - "log_file:%v move_back:%v isos:%v skip_empty:%v wait_ext:%v max_bytes:%s files:%d ratio:%g "+ + "log_file:%v move_back:%v isos:%v preserve_exec:%v skip_empty:%v wait_ext:%v max_bytes:%s files:%d ratio:%g "+ "nested:%d extras_depth:%d symlinks:%v poll:%v event_buffer:%d", folder.Path, epath, folder.DeleteAfter, folder.DeleteOrig, folder.DeleteFiles, - !folder.DisableLog, folder.MoveBack, folder.ExtractISOs, folder.SkipEmpty, folder.WaitExtensions, + !folder.DisableLog, folder.MoveBack, folder.ExtractISOs, folder.PreserveExec, folder.SkipEmpty, folder.WaitExtensions, logMaxBytes(folder.MaxBytes, "uncapped"), folder.MaxFiles, folder.MaxRatio, folder.MaxNested, folder.ExtrasMaxDepth, folder.AllowSymlinks, folder.Interval, u.Folder.Buffer) } else { @@ -195,10 +195,10 @@ func (u *Unpackerr) logFolders(printf configLine) { } printf(" => Path: %s%s; delete_after:%v delete_orig:%v delete_files:%v log_file:%v "+ - "move_back:%v isos:%v skip_empty:%v wait_ext:%v max_bytes:%s files:%d ratio:%g "+ + "move_back:%v isos:%v preserve_exec:%v skip_empty:%v wait_ext:%v max_bytes:%s files:%d ratio:%g "+ "nested:%d extras_depth:%d symlinks:%v poll:%v", folder.Path, epath, folder.DeleteAfter, folder.DeleteOrig, folder.DeleteFiles, - !folder.DisableLog, folder.MoveBack, folder.ExtractISOs, folder.SkipEmpty, folder.WaitExtensions, + !folder.DisableLog, folder.MoveBack, folder.ExtractISOs, folder.PreserveExec, folder.SkipEmpty, folder.WaitExtensions, logMaxBytes(folder.MaxBytes, "uncapped"), folder.MaxFiles, folder.MaxRatio, folder.MaxNested, folder.ExtrasMaxDepth, folder.AllowSymlinks, folder.Interval) } diff --git a/pkg/unpackerr/folder.go b/pkg/unpackerr/folder.go index be013055..01d7d59b 100644 --- a/pkg/unpackerr/folder.go +++ b/pkg/unpackerr/folder.go @@ -116,6 +116,7 @@ func (u *Unpackerr) extractTrackedItem(name string, folder *Folder, now time.Tim Path: name, ExcludeSuffix: exclude, AllowSymlinks: folder.Config.AllowSymlinks, + PreserveExec: folder.Config.PreserveExec, MaxBytes: folder.Config.ResolvedMaxBytes, MaxFiles: folder.Config.MaxFiles, MaxRatio: folder.Config.MaxRatio, diff --git a/pkg/unpackerr/openapi.json b/pkg/unpackerr/openapi.json index 04196eae..a89b0fe8 100644 --- a/pkg/unpackerr/openapi.json +++ b/pkg/unpackerr/openapi.json @@ -382,6 +382,7 @@ "move_back": {"type": "boolean"}, "delete_after": {"type": "string", "nullable": true, "description": "Go duration; null omits"}, "extract_isos": {"type": "boolean"}, + "preserve_exec": {"type": "boolean", "description": "Keep archive execute bits. Off, files use file_mode only."}, "disableRecursion": {"type": "boolean"}, "maxNested": {"type": "integer"}, "extrasMaxDepth": {"type": "integer"},