diff --git a/.golangci.yml b/.golangci.yml index a0511f41..02b94faa 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -12,14 +12,20 @@ linters: - tagliatelle - cyclop - testpackage -run: - timeout: 3m issues: max-issues-per-linter: 0 max-same-issues: 0 + exclude-rules: + # Exclude some linters from testing files. + - linters: + - forbidigo + - lll + path: 'init/config/.*.\.go' output: sort-results: true +run: + timeout: 3m linters-settings: ireturn: @@ -41,4 +47,5 @@ linters-settings: - github.com/radovskyb/watcher - github.com/prometheus/client_golang/ - github.com/spf13/pflag - - github.com/julienschmidt/httprouter \ No newline at end of file + - github.com/julienschmidt/httprouter + - github.com/BurntSushi/toml diff --git a/go.mod b/go.mod index 49060e75..0fd5d18b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/Unpackerr/unpackerr go 1.22 require ( + github.com/BurntSushi/toml v1.4.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gen2brain/dlgs v0.0.0-20220603100644-40c77870fa8d github.com/getlantern/systray v1.2.2 @@ -20,11 +21,10 @@ require ( golift.io/starr v1.0.0 golift.io/version v0.0.2 golift.io/xtractr v0.2.3-0.20240710043203-2d7c8a38d931 - + gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/BurntSushi/toml v1.4.0 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bodgit/plumbing v1.3.0 // indirect @@ -68,5 +68,4 @@ require ( golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.34.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/init/config/README.md b/init/config/README.md new file mode 100644 index 00000000..f8e082fa --- /dev/null +++ b/init/config/README.md @@ -0,0 +1,3 @@ +- All params must have a default, even if it's `[]` or `''`. +- Examples override params, but get commented out. +- All list params are commented. diff --git a/init/config/compose-builder.go b/init/config/compose-builder.go new file mode 100644 index 00000000..f0c258d5 --- /dev/null +++ b/init/config/compose-builder.go @@ -0,0 +1,117 @@ +package main + +import ( + "bytes" + "fmt" + "strings" +) + +const ( + space = " " + composeHeader = `### Unpackerr docker-compose.yml Example +### Please read this page for help using this example: +### https://unpackerr.zip/docs/install/compose +### Generator: https://notifiarr.com/unpackerr +################################################################## +services: + + unpackerr: + image: golift/unpackerr + container_name: unpackerr + volumes: + # You need at least this one volume mapped so Unpackerr can find your files to extract. + # Make sure this matches your Starr apps; the folder mount (/downloads or /data) should be identical. + - /mnt/HostDownloads:/downloads + restart: always + # Get the user:group correct so unpackerr can read and write to your files. + user: ${PUID}:${PGID} + #user: 1000:100 + # What you see below are defaults for this compose. You only need to modify things specific to your environment. + # Remove apps and feature configs you do not use or need. + # ie. Remove all lines that begin with UN_CMDHOOK, UN_WEBHOOK, UN_FOLDER, UN_WEBSERVER, and other apps you do not use. + environment: + - TZ=${TZ}` +) + +func printCompose(config *Config) { + fmt.Println(composeHeader) + + // Loop the 'Order' list. + for _, section := range config.Order { + // If Order contains a missing section, panic. + if config.Sections[section] == nil { + panic(section + ": in order, but missing from sections. This is a bug in conf-builder.yml.") + } + + if config.Defs[section] == nil { + fmt.Print(config.Sections[section].makeCompose(config.Sections[section].Title, config.Prefix, false)) + } else { + fmt.Print(config.Sections[section].makeComposeDefined(config.Prefix, config.Defs[section], config.DefOrder[section], false)) + } + } +} + +func (h *Header) makeCompose(title, prefix string, bare bool) string { + var buf bytes.Buffer + + if len(h.Params) > 0 && bare { + buf.WriteString("## " + title + "\n") + } else if len(h.Params) > 0 { + buf.WriteString(space + " ## " + title + "\n") + } + + pfx := space + " - " + if bare { + pfx = "" + } + + for _, param := range h.Params { + if h.Kind == list { + buf.WriteString(param.Compose(pfx + prefix + h.Prefix + "0_")) + } else { + buf.WriteString(param.Compose(pfx + prefix + h.Prefix)) + } + } + + return buf.String() +} + +func (h *Header) makeComposeDefined(prefix string, defs Defs, order []section, bare bool) string { + var buf bytes.Buffer + + for _, section := range order { + newHeader := createDefinedSection(defs[section], h) + // Make a brand new section and print it. + buf.WriteString(newHeader.makeCompose(h.Title, prefix, bare)) + } + + return buf.String() +} + +func (p *Param) Compose(prefix string) string { + val := p.Default + if p.Example != nil { + val = p.Example + } + + switch p.Kind { + default: + return fmt.Sprint(prefix, p.EnvVar, "=", val, "\n") + case list: + var out string + + for idx, sv := range val.([]any) { //nolint:forcetypeassert + out += fmt.Sprint(prefix, p.EnvVar, idx, "=", sv, "\n") + } + + return out + case "conlist": + out := []string{} + + for _, sv := range val.([]any) { //nolint:forcetypeassert + out = append(out, fmt.Sprint(sv)) + } + + return fmt.Sprint(prefix, p.EnvVar, "=", strings.Join(out, ","), "\n") + } +} diff --git a/init/config/conf-builder.go b/init/config/conf-builder.go new file mode 100644 index 00000000..78e2aa08 --- /dev/null +++ b/init/config/conf-builder.go @@ -0,0 +1,109 @@ +package main + +import ( + "bytes" + "fmt" + "strings" + + "github.com/BurntSushi/toml" +) + +func printConfFile(config *Config) { + // Loop the 'Order' list. + for _, section := range config.Order { + // If Order contains a missing section, panic. + if config.Sections[section] == nil { + panic(section + ": in order, but missing from sections. This is a bug in conf-builder.yml.") + } + + if config.Defs[section] != nil { + fmt.Print(config.Sections[section].makeDefinedSection(config.Defs[section], config.DefOrder[section], false)) + } else { + fmt.Print(config.Sections[section].makeSection(section, false, false)) + } + } +} + +// Not all sections have defs, and it may be nil. Defs only work on 'list' sections. +func (h *Header) makeSection(name section, showHeader, showValue bool) string { + var buf bytes.Buffer + + // Print section header text. + if h.Text != "" { + buf.WriteString(h.Text) + } + + comment := "#" + if showHeader { + // this only happens when a defined section has a comment override on the repeating headers. + comment = "" + } + + if !h.NoHeader { // Print the [section] or [[section]] header. + if h.Kind == list { // list sections are commented by default. + buf.WriteString(comment + "[[" + string(name) + "]]" + "\n") // list sections use double-brackets. + } else { + buf.WriteString("[" + string(name) + "]" + "\n") // non-list sections use single brackets. + } + } + + for _, param := range h.Params { + // Print an empty newline for each param if the section has no header and the param has a description. + if h.NoHeader && param.Desc != "" { + buf.WriteString("\n") + } + + // Add ## to the beginning of each line in the description. + // Uses the newline \n character to figure out where each line begins. + if param.Desc != "" { + buf.WriteString("## " + strings.ReplaceAll(strings.TrimSpace(param.Desc), "\n", "\n## ") + "\n") + } + + switch { + default: + fallthrough + case showValue: + buf.WriteString(fmt.Sprintf("%s = %s\n", param.Name, param.Value())) + case param.Example != nil: + // If example is not empty, use that commented out, otherwise use the default. + fallthrough + case h.Kind == list: + // If the 'kind' is a 'list', we comment all the parameters. + buf.WriteString(fmt.Sprintf("#%s = %s\n", param.Name, param.Value())) + } + } + + // Each section needs a newline at the end. + buf.WriteString("\n") + + return buf.String() +} + +func (p *Param) Value() string { + // If example is not empty, use that commented out, otherwise use the default. + out, _ := toml.Marshal(p.Default) + if p.Example != nil { + out, _ = toml.Marshal(p.Example) + } + + // The toml marshaller uses only regular quotes " which kinda suck, so replace them with single quotes ' on file paths. + if strings.Contains(p.Name, "path") || strings.HasSuffix(p.Name, "file") || p.Name == "command" { + return string(bytes.ReplaceAll(out, []byte{'"'}, []byte("'"))) + } + + return string(out) +} + +// makeDefinedSection duplicates sections from overrides, and prints it once for each override. +func (h *Header) makeDefinedSection(defs Defs, order []section, showValue bool) string { + var buf bytes.Buffer + + for _, section := range order { + newHeader := createDefinedSection(defs[section], h) + // Make a brand new section and pass it back in. + // Only defined sections can comment the header. + buf.WriteString(newHeader.makeSection(section, !defs[section].Comment, showValue)) + } + + return buf.String() +} diff --git a/init/config/conf-builder.yml b/init/config/conf-builder.yml new file mode 100644 index 00000000..d5a34b2c --- /dev/null +++ b/init/config/conf-builder.yml @@ -0,0 +1,804 @@ +# Unpackerr Config File Definition +--- + +envvar_prefix: UN_ +order: + - global + - webserver + - folders + - starr_header + - starr + - folder + - webhook + - cmdhook +def_order: + starr: + - sonarr + - radarr + - lidarr + - readarr + - whisparr +recommendations: + apps: &APPS + - name: Sonarr + value: sonarr + - name: Radarr + value: radarr + - name: Lidarr + value: lidarr + - name: Readarr + value: readarr + - name: Whisparr + value: whisparr + - name: Folder + value: folder + on_off: &BOOLEAN + - name: Yes + value: true + - name: No + value: false + event_ids: &EVENT_IDS + - name: All Events + value: 0 + - name: Queues + value: 1 + - name: Extracting + value: 2 + - name: Extract Failed + value: 3 + - name: Extracted + value: 4 + - name: Imported + value: 5 + - name: Deleting + value: 6 + - name: Delete Failed + value: 7 + - name: Deleted + value: 8 + - name: Nothing Extracted + value: 9 + global: &GLOBAL_INTERVALS + - name: 1 minute + value: 1m + - name: 2 minutes + value: 2m + - name: 3 minutes + value: 3m + - name: 4 minutes + value: 4m + - name: 5 minutes + value: 5m + - name: 10 minutes + value: 10m + - name: 15m minutes + value: 15m + - name: 20 minutes + value: 20m + timeout: &TIMEOUTS + - name: 10 seconds + value: 10s + - name: 15 seconds + value: 15s + - name: 20 seconds + value: 20s + - name: 30 seconds + value: 30s + - name: 45 seconds + value: 45s + - name: 1 minute + value: 1m + - name: 1.5 minutes + value: 90s + - name: 2 minutes + value: 2m + - name: 3 minutes + value: 3m + - name: 5 minutes + value: 5m + queues: &QUEUE_INTERVALS + - name: 1 minute + value: 1m + - name: 2 minutes + value: 2m + - name: 3 minutes + value: 3m + - name: 4 minutes + value: 4m + - name: 5 minutes + value: 5m + - name: 10 minutes + value: 10m + - name: 15m minutes + value: 15m + - name: 20 minutes + value: 20m + - name: 2 hours + value: 2h + - name: 6 hours + value: 6h + - name: 12 hours + value: 12h + - name: 24 hours + value: 24h + folders: &FOLDER_INTERVALS + - name: Disabled + value: 1ms + - name: 1/2 second + value: 500ms + - name: 1 second + value: 1s + - name: 2 seconds + value: 2s + - name: 3 seconds + value: 3s + - name: 5 seconds + value: 5s + - name: 8 seconds + value: 8s + - name: 10 seconds + value: 10s + - name: 15 seconds + value: 15s + - name: 30 seconds + value: 30s + - name: 1 minute + value: 1m + - name: 2 minutes + value: 2m + +defs: + starr: + sonarr: + title: Sonarr Settings + prefix: SONARR_ + text: | + ## Leaving the [[sonarr]] header uncommented (no leading hash #) without also + ## uncommenting the api_key (remove the hash #) will produce a startup warning. + defaults: + url: http://127.0.0.1:8989 + radarr: + title: Radarr Settings + prefix: RADARR_ + text: | + ## Leaving the [[radarr]] header uncommented (no leading hash #) without also + ## uncommenting the api_key (remove the hash #) will produce a startup warning. + defaults: + url: http://127.0.0.1:7878 + lidarr: + title: Lidarr Settings + prefix: LIDARR_ + comment: true + defaults: + url: http://127.0.0.1:8686 + readarr: + title: Readarr Settings + prefix: READARR_ + comment: true + defaults: + url: http://127.0.0.1:8787 + whisparr: + title: Whisparr Settings + prefix: WHISPARR_ + comment: true + defaults: + url: http://127.0.0.1:6969 + +sections: + + global: + title: "Global Settings" + no_header: true + text: | + ####################################################### + ## Unpackerr Example Configuration File ## + ####################################################### + ## The displayed values are application defaults. ## + ## Environment Variables may override all values. ## + ## More configuration help: https://unpackerr.zip ## + ## Config Generator: https://notifiarr.com/unpackerr ## + ####################################################### + docs: | + These values must exist at the top of the config file. + If you put them anywhere else they may be attached to a `[header]` inadvertently. + When using environment variables, you can simply omit the ones you don't set or change from default. + params: + - name: debug + envvar: DEBUG + default: false + recommend: *BOOLEAN + short: Turns on more logs. + desc: | + Turn on debug messages in the output. Do not wrap this in quotes. + Recommend trying this so you know what it looks like. I personally leave it on. + - name: quiet + envvar: QUIET + default: false + recommend: *BOOLEAN + short: Do not print logs to stdout or stderr. + desc: | + Disable writing messages to stdout/stderr. This silences the app. Set a log + file below if you set this to true. Recommended when starting with systemctl. + - name: error_stderr + envvar: ERROR_STDERR + default: false + recommend: *BOOLEAN + short: Print ERROR lines to stderr instead of stdout. + desc: | + Send error output to stderr instead of stdout by setting error_stderr to true. + Recommend leaving this at false. Ignored if quiet (above) is true. + - name: activity + envvar: ACTIVITY + default: false + recommend: *BOOLEAN + short: Setting true will print only queue counts with activity. + desc: | + Setting activity to true will silence all app queue log lines with only zeros. + Set this to true when you want less log spam. + - name: log_queues + envvar: LOG_QUEUES + default: "1m" + short: How often to print internal counters. Uses Go Duration. + recommend: *QUEUE_INTERVALS + desc: | + The Starr-application activity queue is logged on an interval. + Adjust that interval with this setting. + Default is a minute. 2m, 5m, 10m, 30m, 1h are also perfectly acceptable. + - name: log_file + envvar: LOG_FILE + default: '' + example: /downloads/unpackerr.log + short: Provide optional file path to write logs + desc: | + Write messages to a log file. This is the same data that is normally output to stdout. + This setting is great for Docker users that want to export their logs to a file. + The alternative is to use syslog to log the output of the application to a file. + Default is no log file; this is unset. + Except on macOS and Windows, the log file gets set to "~/.unpackerr/unpackerr.log" + log_files=0 turns off auto-rotation. + Default files is 10 and size(mb) is 10 Megabytes; both doubled if debug is true. + - name: log_files + envvar: LOG_FILES + default: 10 + recommend: &NUMBERS + - value: 1 + - value: 2 + - value: 3 + - value: 5 + - value: 10 + - value: 15 + - value: 20 + short: Log files to keep after rotating. `0` disables rotation + - name: log_file_mb + envvar: LOG_FILE_MB + default: 10 + recommend: *NUMBERS + short: Max size of log files in megabytes + - name: interval + envvar: INTERVAL + default: 2m + recommend: *GLOBAL_INTERVALS + short: How often apps are polled, recommend `1m` to `5m`. + desc: | + How often to poll starr apps (sonarr, radarr, etc). + Recommend 1m-5m. Uses Go Duration. + - name: start_delay + envvar: START_DELAY + default: 1m + recommend: *GLOBAL_INTERVALS + short: Files are queued at least this long before extraction. + desc: | + How long an item must be queued (download complete) before extraction will start. + One minute is the historic default and works well. Set higher if your downloads + take longer to finalize (or transfer locally). Uses Go Duration. + - name: retry_delay + envvar: RETRY_DELAY + default: 5m + recommend: *GLOBAL_INTERVALS + short: Failed extractions are retried after at least this long. + desc: | + How long to wait before removing the history for a failed extraction. + Once the history is deleted the item will be recognized as new and + extraction will start again. Uses Go Duration. + - name: max_retries + envvar: MAX_RETRIES + default: 3 + recommend: *NUMBERS + short: Failed extractions are retried after at least this long. + desc: | + How many times to retry a failed extraction. Pauses retry_delay between attempts. + - name: parallel + envvar: PARALLEL + default: 1 + recommend: *NUMBERS + short: Times to retry failed extractions. `0` = unlimited. + desc: | + How many files may be extracted in parallel. 1 works fine. + Do not wrap the number in quotes. Raise this only if you have fast disks and CPU. + - name: file_mode + envvar: FILE_MODE + default: '0644' + recommend: + - value: '0600' + - value: '0640' + - value: '0660' + - value: '0644' + - value: '0640' + short: Extracted files are written with this mode. + desc: | + Use these configurations to control the file modes used for newly extracted + files and folders. Recommend 0644/0755 or 0666/0777. + - name: dir_mode + envvar: DIR_MODE + default: '0755' + recommend: + - value: '0700' + - value: '0750' + - value: '0755' + - value: '0770' + - value: '0775' + short: Extracted folders are written with this mode + + webserver: + title: Web Server + docs: | + :::note Metrics + The web server currently only provides prometheus metrics, which you can display in + [Grafana](https://grafana.com/grafana/dashboards/18817-unpackerr/). + It provides no UI. This may change in the future. The web server was added in v0.12.0. + ::: + envvar_prefix: WEBSERVER_ + params: + - name: metrics + envvar: METRICS + default: false + recommend: *BOOLEAN + short: Extracted folders are written with this mode + desc: The web server currently only supports metrics; set this to true if you wish to use it. + - name: listen_addr + envvar: LISTEN_ADDR + default: 0.0.0.0:5656 + short: ip:port to listen on; `0.0.0.0` is all IPs. + desc: This may be set to a port or an ip:port to bind a specific IP. 0.0.0.0 binds ALL IPs. + - name: log_file + envvar: LOG_FILE + default: '' + short: Provide optional file path to write HTTP logs. + desc: Recommend setting a log file for HTTP requests. Otherwise, they go with other logs. + - name: log_files + envvar: LOG_FILES + default: 10 + recommend: *NUMBERS + short: Log files to keep after rotating. `0` to disable. + desc: This app automatically rotates logs. Set these to the size and number to keep. + - name: log_file_mb + envvar: LOG_FILE_MB + default: 10 + recommend: *NUMBERS + short: Max size of HTTP log files in megabytes + - name: ssl_cert_file + envvar: SSL_CERT_FILE + default: '' + short: Path to SSL cert file to serve HTTPS. + desc: Set both of these to valid file paths to enable HTTPS/TLS. + - name: ssl_key_file + envvar: SSL_KEY_FILE + default: '' + short: Path to SSL key file to serve HTTPS. + - name: urlbase + envvar: URLBASE + default: / + short: Base URL path to serve HTTP content. + desc: Base URL from which to serve content. + - name: upstreams + envvar: UPSTREAMS + default: [] + kind: conlist + short: List of upstream proxy CIDRs or IPs to trust. + desc: | + Upstreams should be set to the IP or CIDR of your trusted upstream proxy. + Setting this correctly allows X-Forwarded-For to be used in logs. + In the future it may control auth proxy trust. Must be a list of strings. + example: upstreams = [ "127.0.0.1/32", "10.1.2.0/24" ] + + starr_header: + no_header: true + text: | + ############################################################################### + ##-IMPORTANT-#######-READ THIS!!!-################ Seriously, read this. ###### + ############################################################################### + ## The following sections can be repeated if you have more than one Sonarr, ## + ## Radarr, Lidarr, Readarr, Whisparr, Folder, Webhook, and/or Command Hook. ## + ## You MUST uncomment the [[header]], url and api_key at for any Starr app. ## + ## The [[sonarr]] and [[radarr]] headers come uncommented. Uncomment the url ## + ## and api_key if they are in use. Comment them with a hash if they are not. ## + ## Uncomment the [[lidarr]] and/or [[readarr]] headers and values if in use. ## + ############################################################################### + ############################################################################### + ## ALL LINES BEGINNING WITH A HASH # ARE IGNORED COMMENTS ## + ## REMOVE THE HASH # FROM CONFIG LINES YOU WANT TO CHANGE ## + ############################################################################### + ############################################################################### + + starr: + kind: list + params: + - name: url + envvar: URL + short: URL where this starr app can be accessed. + - name: api_key + envvar: API_KEY + default: '' + example: 0123456789abcdef0123456789abcdef + short: Provide URL and API key if you use this app. + - name: paths + envvar: PATHS_ + default: ['/downloads'] + kind: list + short: File system path where downloaded items are located. + desc: | + List of paths where content is downloaded for this app. + Used as fallback if the path the Starr app reports does not exist or is not accessible. + - name: protocols + envvar: PROTOCOLS + default: torrent + recommend: + - value: torrent + - value: torrent,usenet + - value: usenet + short: 'Protocols to process. Alt: `torrent,usenet`' + desc: 'Default protocols is torrent. Alternative: torrent,usenet' + - name: timeout + envvar: TIMEOUT + default: 10s + recommend: *TIMEOUTS + short: How long to wait for the app to respond. + desc: How long to wait for a reply from the backend. + - name: delete_delay + envvar: DELETE_DELAY + default: 5m + recommend: *GLOBAL_INTERVALS + short: Extracts are deleted this long after import, `-1s` to disable. + desc: How long to wait after import before deleting the extracted items. + - name: delete_orig + envvar: DELETE_ORIG + default: false + recommend: *BOOLEAN + short: Delete archives after import? Recommend keeping this false. + desc: | + If you use this app with NZB you may wish to delete archives after extraction. + General recommendation is: do not enable this for torrent use. + Setting this to true deletes the entire original download folder after import. + - name: syncthing + envvar: SYNCTHING + default: false + recommend: *BOOLEAN + short: Setting this to true makes unpackerr wait for syncthing to finish. + desc: If you use Syncthing, setting this to true will make unpackerr wait for syncs to finish. + + # Global folder configuration. + folders: + title: Folder Settings + text: | + ## Global Folder configuration that affects all watched folders. + envvar_prefix: FOLDERS_ + params: + - name: interval + envvar: INTERVAL + default: 0s + short: How often poller checks for new folders. Use `1ms` to disable it. + recommend: *FOLDER_INTERVALS + desc: | + How often poller checks for new folders. + The default of `0s` will disable the poller on all systems except Docker. + Set this value to `1ms` to disable it in Docker. + - name: buffer + envvar: BUFFER + default: 20000 + short: How many new folder events can be immediately queued. + desc: How many new folder events can be immediately queued. Don't change this. + + # Per-folder configuration (list). + folder: + title: Watch Folders + text: | + ################################################################################## + ### ### STOP HERE ### STOP HERE ### STOP HERE ### STOP HERE #### STOP HERE ### # + ### Only using Starr apps? The things above. The below configs are OPTIONAL. ### # + ################################################################################## + + + ##-Folders-####################################################################### + ## This application can also watch folders for things to extract. If you copy a ## + ## subfolder into a watched folder (defined below) any extractable items in the ## + ## folder will be decompressed. This has nothing to do with Starr applications. ## + ################################################################################## + docs: | + Folders are a way to watch a folder for things to extract. You can use this to + monitor your download client's "move to" path if you're not using it with an Starr app. + envvar_prefix: FOLDER_ + kind: list + params: + - name: path + envvar: PATH + default: '' + example: /downloads/auto_extract + short: Folder to watch for archives. **Not for Starr apps.** + - name: extract_path + envvar: EXTRACT_PATH + default: '' + short: Where to extract to. Uses `path` if not set. + desc: Path to extract files to. The default (leaving this blank) is the same as `path` (above). + - name: delete_after + envvar: DELETE_AFTER + default: 10m + recommend: *GLOBAL_INTERVALS + short: Delete requested files after this duration; `0` disables. + desc: | + Delete extracted or original files this long after extraction. + The default is 0. Set to 0 to disable all deletes. Uncomment it to enable deletes. Uses Go Duration. + - name: disable_recursion + envvar: DISABLE_RECURSION + default: false + recommend: *BOOLEAN + short: Setting this to true disables extracting archives inside archives. + desc: Unpackerr extracts archives inside archives. Set this to true to disable recursive extractions. + - name: delete_files + envvar: DELETE_FILES + default: false + recommend: *BOOLEAN + short: Delete extracted files after successful extraction. + desc: Delete extracted files after successful extraction? Honors delete_after. + - name: delete_original + envvar: DELETE_ORIGINAL + default: false + recommend: *BOOLEAN + short: Delete archives after successful extraction. + desc: Delete original items after successful extraction? Honors delete_after. + - name: disable_log + envvar: DISABLE_LOG + default: false + recommend: *BOOLEAN + short: Turns off creation of extraction logs files for this folder. + desc: Disable extraction log (unpackerred.txt) file creation? + - name: move_back + envvar: MOVE_BACK + default: false + recommend: *BOOLEAN + short: Move extracted items back into original folder. + desc: Move extracted files into original folder? If false, files go into an _unpackerred folder. + - name: extract_isos + envvar: EXTRACT_ISOS + default: false + recommend: *BOOLEAN + short: Setting this to true enables .iso file extraction. + desc: Set this to true if you want this app to extract ISO files with .iso extension. + + webhook: + title: Web Hooks + text: | + ################ + ### Webhooks ### + ################ + # Sends a webhook when an extraction queues, starts, finishes, and/or is deleted. + # Created to integrate with notifiarr.com. + # Also works natively with Discord.com, Telegram.org, and Slack.com webhooks. + # Can possibly be used with other services by providing a custom template_path. + ###### Don't forget to uncomment [[webhook]] and url at a minimum !!!! + docs: | + This application can send a `POST` webhook to a URL when an extraction begins, and again + when it finishes. Configure 1 or more webhook URLs with the parameters below. + Works great with [notifiarr.com](https://notifiarr.com). You can use + [requestbin.com](https://requestbin.com/r/) to test and _see_ the payload. + notes: | + - _`Nickname` should equal the `chat_id` value in Telegram webhooks._ + - _`Channel` is used as destination channel for Slack. It's not used in others._ + - _`Nickname` and `Channel` may be used as custom values in custom templates._ + - _`Name` is only used in logs, but it's also available as a template value as `{{name}}`._ + - Built-In Templates: `pushover`, `telegram`, `discord`, `notifiarr`, `slack`, `gotify`. + envvar_prefix: WEBHOOK_ + kind: list + params: + - name: url + envvar: URL + default: '' + example: https://notifiarr.com/api/v1/notification/unpackerr/api_key_from_notifiarr_com + short: URL to send POST webhook to. + - name: name + envvar: NAME + default: '' + short: Provide an optional name to hide the URL in logs. + desc: | + Provide an optional name to hide the URL in logs. + If a name is not provided then the URL is used. + - name: silent + envvar: SILENT + default: false + recommend: *BOOLEAN + short: Hide successful POSTs from logs. + desc: Do not log success (less log spam). + - name: events + envvar: EVENTS_ + default: + - 0 + example: + - 1 + - 4 + - 6 + kind: list + recommend: *EVENT_IDS + short: List of event ids to send notification for, `0` for all. + desc: | + List of event ids to send notification for, [0] for all. + The default is [0] and this is an example: + - name: nickname + envvar: NICKNAME + default: 'Unpackerr' + short: Passed into templates for telegram, discord and slack hooks. + desc: | + ===> Advanced Optional Webhook Configuration <=== + Used in Discord and Slack templates as bot name, in Telegram as chat_id. + - name: channel + envvar: CHANNEL + default: '' + short: Passed into templates for slack.com webhooks. + desc: Also passed into templates. Used in Slack templates for destination channel. + - name: exclude + envvar: EXCLUDE_ + default: [] + example: ["readarr", "lidarr"] + recommend: *APPS + kind: list + short: 'List of apps to exclude: radarr, sonarr, folders, etc.' + desc: 'List of apps to exclude. None by default. This is an example:' + - name: template_path + envvar: TEMPLATE_PATH + default: '' + short: Instead of an internal template, provide your own. + desc: Override internal webhook template for discord.com or other hooks. + - name: template + envvar: TEMPLATE + default: '' + recommend: + - value: "notifiarr" + - value: "discord" + - value: "telegram" + - value: "gotify" + - value: "pushover" + - value: "slack" + short: Instead of auto template selection, force a built-in template. + desc: 'Override automatic template detection. Values: notifiarr, discord, telegram, gotify, pushover, slack' + - name: ignore_ssl + envvar: IGNORE_SSL + default: false + recommend: *BOOLEAN + short: Ignore invalid SSL certificates. + desc: Set this to true to ignore the SSL certificate on the server. + - name: timeout + envvar: TIMEOUT + default: 10s + recommend: *TIMEOUTS + short: How long to wait for server response. + desc: You can adjust how long to wait for a server response. + - name: content_type + envvar: CONTENT_TYPE + default: application/json + recommend: + - value: "application/json" + - value: "application/x-yaml" + - value: "application/xml" + - value: "application/x-www-form-urlencoded" + short: Content-Type header sent to webhook. + desc: If your custom template uses another MIME type, set this. + + cmdhook: + title: Command Hooks + docs: | + Unpackerr can execute commands (or scripts) before and after an archive extraction. + The only thing required is a command. Name is optional, and used in logs only. + Setting `shell` to `true` executes your command after `/bin/sh -c` or `cmd.exe /c` + on Windows. + tail: | + All extraction data is input to the command using environment variables, see example below. + Extracted files variables names begin with `UN_DATA_FILES_`. + Try `/usr/bin/env` as an example command to see what variables are available. + +
+ Example Output Variables + + ```none + UN_DATA_OUTPUT=folder/subfolder_unpackerred + UN_PATH=folder/subfolder + UN_DATA_START=2021-10-04T23:04:27.849216-07:00 + UN_REVISION= + UN_EVENT=extracted + UN_GO=go1.17 + UN_DATA_ARCHIVES=folder/subfolder_unpackerred/Funjetting.rar,folder/subfolder_unpackerred/Funjetting.r00,folder/subfolder/files.zip + UN_DATA_ARCHIVE_2=folder/subfolder/files.zip + UN_DATA_ARCHIVE_1=folder/subfolder_unpackerred/Funjetting.r00 + UN_DATA_ARCHIVE_0=folder/subfolder_unpackerred/Funjetting.rar + UN_DATA_FILES=folder/subfolder/Funjetting.mp3,folder/subfolder/Funjetting.r00,folder/subfolder/Funjetting.rar,folder/subfolder/_unpackerred.subfolder.txt + UN_DATA_FILE_1=folder/subfolder/Funjetting.r00 + UN_DATA_BYTES=2407624 + PWD=/Users/david/go/src/github.com/Unpackerr/unpackerr + UN_DATA_FILE_0=folder/subfolder/Funjetting.mp3 + UN_OS=darwin + UN_DATA_FILE_3=folder/subfolder/_unpackerred.subfolder.txt + UN_DATA_FILE_2=folder/subfolder/Funjetting.rar + UN_BRANCH= + UN_TIME=2021-10-04T23:04:27.869613-07:00 + UN_VERSION= + UN_DATA_QUEUE=0 + SHLVL=1 + UN_APP=Folder + UN_STARTED=2021-10-04T23:03:22.849253-07:00 + UN_ARCH=amd64 + UN_DATA_ELAPSED=20.365752ms + UN_DATA_ERROR= + ``` + +
+ text: | + ##################### + ### Command Hooks ### + ##################### + # Executes a script or command when an extraction queues, starts, finishes, and/or is deleted. + # All data is passed in as environment variables. Try /usr/bin/env to see what variables are available. + ###### Don't forget to uncomment [[cmdhook]] at a minimum !!!! + envvar_prefix: CMDHOOK_ + kind: list + params: + - name: command + envvar: COMMAND + default: '' + example: /downloads/scripts/command.sh + short: Command to run. + - name: name + envvar: NAME + default: '' + short: Name for logs, otherwise uses first word in command. + desc: | + Provide an optional name to hide the URL in logs. + If a name is not provided the first word in the command is used. + - name: shell + envvar: SHELL + default: false + recommend: *BOOLEAN + short: Run command inside a shell. + desc: Runs the command inside /bin/sh ('nix) or cmd.exe (Windows). + - name: silent + envvar: SILENT + default: false + recommend: *BOOLEAN + short: Hide command output from logs. + desc: Do not log command's output. + - name: events + envvar: EVENTS_ + default: + - 0 + recommend: *EVENT_IDS + example: + - 1 + - 4 + - 7 + kind: list + short: List of event ids to run command for, `0` for all. + desc: | + List of event ids to run command for, [0] for all. + The default is [0] and this is an example: + - name: exclude + envvar: EXCLUDE_ + default: [] + example: ["readarr", "lidarr"] + recommend: *APPS + kind: list + short: 'List of apps to exclude: radarr, sonarr, folders, etc.' + desc: | + ===> Optional Command Hook Configuration <=== + List of apps to exclude. None by default. This is an example: + - name: timeout + envvar: TIMEOUT + default: 10s + recommend: *TIMEOUTS + short: How long to wait for the command to run. + desc: You can adjust how long to wait for the command to run. diff --git a/init/config/docs-builder.go b/init/config/docs-builder.go new file mode 100644 index 00000000..2314681e --- /dev/null +++ b/init/config/docs-builder.go @@ -0,0 +1,147 @@ +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "time" + + "github.com/BurntSushi/toml" +) + +const ( + outputDir = "generated/" + dirMode = 0o755 + fileMode = 0o644 +) + +func printDocusaurus(config *Config) { + // Loop the 'Order' list. + if err := makeGenerated(config); err != nil { + panic(err) + } + + for _, section := range config.Order { + // If Order contains a missing section, panic. + if config.Sections[section] == nil { + panic(section + ": in order, but missing from sections. This is a bug in conf-builder.yml.") + } + + if len(config.Sections[section].Params) < 1 { + continue + } + + if config.Defs[section] != nil { + data := config.Sections[section].makeDefinedDocs(config.Prefix, config.Defs[section], config.DefOrder[section]) + if err := output(string(section), data); err != nil { + panic(err) + } + } else { + data := config.Sections[section].makeDocs(config.Prefix, section) + if err := output(string(section), data); err != nil { + panic(err) + } + } + } +} + +func output(file, content string) error { + _ = os.Mkdir(outputDir, dirMode) + date := "---\n# Generated: " + time.Now().Round(time.Second).String() + "\n---\n\n" + //nolint:wrapcheck + return os.WriteFile(filepath.Join(outputDir, file+".md"), []byte(date+content), fileMode) +} + +// makeGenerated writes a special file that the website can import. +// Adds all param sections except global into a docusaurus import format. +func makeGenerated(config *Config) error { + var ( + first bytes.Buffer + second bytes.Buffer + ) + + for _, section := range config.Order { + if len(config.Sections[section].Params) > 0 && section != "global" { + title := "G" + string(section) + first.WriteString("import " + title + " from './" + string(section) + ".md';\n") + second.WriteString("<" + title + "/>\n") + } + } + + return output("index", first.String()+"\n"+second.String()) +} + +func (h *Header) makeDocs(prefix string, section section) string { + buf := bytes.Buffer{} + buf.WriteString("## " + h.Title + "\n\n
\n") + + conf := h.makeSection(section, true, true) + env := h.makeCompose(h.Title, prefix, true) + header := "[" + string(section) + "]" + + if h.Kind == list { + header = "[[" + string(section) + "]]" + } + + if h.NoHeader { + buf.WriteString(" Examples. Prefix: " + prefix + "\n\n") + } else { + buf.WriteString(" Examples. Prefix: " + prefix + h.Prefix + ", Header: " + header + "\n\n") + } + + buf.WriteString("- Using the config file:\n\n```yaml\n") + buf.WriteString(strings.TrimSpace(conf) + "\n```\n\n") + buf.WriteString("- Using environment variables:\n\n```js\n") + buf.WriteString(env + "```\n\n
\n\n") + buf.WriteString(h.Docs + "\n") // Docs comes before the table. + buf.WriteString(h.makeDocsTable(prefix) + "\n") + buf.WriteString(h.Tail) // Tail goes after the docs and table. + + if h.Notes != "" { // Notes become a sub header. + buf.WriteString("### " + h.Title + " Notes\n\n" + h.Notes) + } + + return buf.String() +} + +func (h *Header) makeDocsTable(prefix string) string { + const ( + tableHeader = "|Config Name|Variable Name|Default / Note|\n|---|---|---|\n" + tableFormat = "|%s|`%s`|%v / %s|\n" + ) + + buf := bytes.Buffer{} + buf.WriteString(tableHeader) + + for _, param := range h.Params { + envVar := prefix + h.Prefix + param.EnvVar + if param.Kind == list { + envVar += "0" + } + + def := "No Default" + + if rv := reflect.ValueOf(param.Default); rv.Kind() == reflect.Bool || !rv.IsZero() { + if t, _ := toml.Marshal(param.Default); len(t) > 0 { + def = "`" + string(t) + "`" + } + } + + buf.WriteString(fmt.Sprintf(tableFormat, param.Name, envVar, def, param.Short)) + } + + return buf.String() +} + +func (h *Header) makeDefinedDocs(prefix string, defs Defs, order []section) string { + var buf bytes.Buffer + + for _, section := range order { + buf.WriteString(createDefinedSection(defs[section], h).makeDocs(prefix, section)) + } + + return buf.String() +} diff --git a/init/config/main.go b/init/config/main.go new file mode 100644 index 00000000..ac74f2b5 --- /dev/null +++ b/init/config/main.go @@ -0,0 +1,142 @@ +package main + +import ( + "fmt" + "io" + "net/http" + "os" + "strings" + "time" + + "gopkg.in/yaml.v3" +) + +const ( + list = "list" + // inputFile = "https://raw.githubusercontent.com/Unpackerr/unpackerr/main/init/config/conf-builder.yml" + inputFile = "https://raw.githubusercontent.com/Unpackerr/unpackerr/dn2_conf_builder/init/config/conf-builder.yml" + opTimeout = 6 * time.Second +) + +type section string + +type Option struct { + Name string `yaml:"name"` + Value any `yaml:"value"` +} + +type Config struct { + DefOrder map[section][]section `yaml:"def_order"` + Defs map[section]Defs `yaml:"defs"` + Prefix string `yaml:"envvar_prefix"` + Order []section `yaml:"order"` + Sections map[section]*Header `yaml:"sections"` +} + +type Header struct { + Tail string `yaml:"tail"` + Title string `yaml:"title"` + Text string `yaml:"text"` + Docs string `yaml:"docs"` + Notes string `yaml:"notes"` + Prefix string `yaml:"envvar_prefix"` + Params []*Param `yaml:"params"` + Kind string `yaml:"kind"` // "", list + NoHeader bool `yaml:"no_header"` // Do not print [section] header. +} + +type Param struct { + Name string `yaml:"name"` + EnvVar string `yaml:"envvar"` + Default any `yaml:"default"` + Example any `yaml:"example"` + Short string `yaml:"short"` + Desc string `yaml:"desc"` + Kind string `yaml:"kind"` // "", list, conlist + Recommend []Option `yaml:"recommend"` +} + +type Def struct { + Comment bool `yaml:"comment"` // just the header. + Title string `yaml:"title"` + Prefix string `yaml:"prefix"` + Text string `yaml:"text"` + Defaults map[string]any `yaml:"defaults"` +} + +type Defs map[section]*Def + +func main() { + file, err := openFile() + if err != nil { + panic(err) + } + defer file.Close() + + config := &Config{} + // Decode conf-builder file into Go data structure. + if err = yaml.NewDecoder(file).Decode(config); err != nil { + panic(err) + } + + switch { + default: + fallthrough + case len(os.Args) <= 1: + fallthrough + case os.Args[1] == "conf": + printConfFile(config) + case os.Args[1] == "compose", os.Args[1] == "docker": + printCompose(config) + case os.Args[1] == "docs": + printDocusaurus(config) + } +} + +// openFile opens a file or url for the parser. +func openFile() (io.ReadCloser, error) { + fileName := inputFile + if len(os.Args) > 2 { //nolint:mnd + fileName = os.Args[len(os.Args)-1] // take last arg as file. + } + + if strings.HasPrefix(fileName, "http") { + http.DefaultClient.Timeout = opTimeout + + resp, err := http.Get(fileName) //nolint:noctx // because we set a timeout. + if err != nil { + return nil, fmt.Errorf("%s: %w", fileName, err) + } + + return resp.Body, nil + } + + file, err := os.Open(fileName) + if err != nil { + return nil, fmt.Errorf("%s: %w", fileName, err) + } + + return file, nil +} + +func createDefinedSection(def *Def, section *Header) *Header { + newSection := &Header{ + Text: def.Text, + Prefix: def.Prefix, + Title: def.Title, + Params: section.Params, + Kind: section.Kind, + } + + // Loop each defined section Defaults, and see if one of the param names match. + for overrideName, override := range def.Defaults { + for _, defined := range newSection.Params { + // If the name of the default (override) matches this param name, overwrite the value. + if defined.Name == overrideName { + defined.Default = override + } + } + } + + return newSection +}