diff --git a/.icons/copyparty.svg b/.icons/copyparty.svg new file mode 100644 index 000000000..2c4f0d045 --- /dev/null +++ b/.icons/copyparty.svg @@ -0,0 +1,210 @@ + + + copyparty_logo + + + + + + + + + + + + image/svg+xml + + copyparty_logo + github.com/9001/copyparty + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/registry/djarbz/.images/avatar.png b/registry/djarbz/.images/avatar.png new file mode 100644 index 000000000..f60192032 Binary files /dev/null and b/registry/djarbz/.images/avatar.png differ diff --git a/registry/djarbz/.images/copyparty_screenshot.png b/registry/djarbz/.images/copyparty_screenshot.png new file mode 100644 index 000000000..690c716f9 Binary files /dev/null and b/registry/djarbz/.images/copyparty_screenshot.png differ diff --git a/registry/djarbz/README.md b/registry/djarbz/README.md new file mode 100644 index 000000000..2319441ab --- /dev/null +++ b/registry/djarbz/README.md @@ -0,0 +1,11 @@ +--- +display_name: "Austin" +bio: "IT Pro by day, script kiddie at night." +avatar: "./.images/avatar.png" +github: "djarbz" +status: "community" +--- + +# Austin + +I like to program as a hobby. diff --git a/registry/djarbz/modules/copyparty/README.md b/registry/djarbz/modules/copyparty/README.md new file mode 100644 index 000000000..fdd3ad56e --- /dev/null +++ b/registry/djarbz/modules/copyparty/README.md @@ -0,0 +1,68 @@ +--- +display_name: copyparty +description: A web based file explorer alternative to Filebrowser. +icon: ../../../../.icons/copyparty.svg +verified: false +tags: [files, filebrowser, web, copyparty] +--- + +# copyparty + + + +This module installs Copyparty, an alternative to Filebrowser. +[Copyparty](https://github.com/9001/copyparty) is a portable file server with accelerated resumable uploads, dedup, WebDAV, FTP, TFTP, zeroconf, media indexer, thumbnails++ all in one file, no deps + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" +} +``` + + + +![copyparty-browser-fs8](../../.images/copyparty_screenshot.png) + +## Examples + +### Example 1 + +Some basic command line options: + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + arguments = [ + "-v", "/home/coder/:/home:r", # Share home directory (read-only) + "-v", "${local.repo_dir}:/repo:rw", # Share project directory (read-write) + "-e2dsa", # Enables general file indexing" + ] +} +``` + +### Example 2 + +```tf +module "copyparty" { + count = data.coder_workspace.me.start_count + source = "registry.coder.com/djarbz/copyparty/coder" + version = "1.0.0" + agent_id = coder_agent.example.id + subdomain = true + arguments = [ + "-v", "/tmp:/tmp:r", # Share tmp directory (read-only) + "-v", "/home/coder/:/home:rw", # Share home directory (read-write) + "-v", "${local.root_dir}:/work:A:c,dotsrch", # Share work directory (All Perms) + "-e2dsa", # Enables general file indexing" + "--re-maxage", "900", # Rescan filesystem for changes every SEC + "--see-dots", # Show dotfiles by default if user has correct permissions on volume + "--xff-src=lan", # List of trusted reverse-proxy CIDRs (comma-separated) or `lan` for private IPs. + "--rproxy", "1", # Which ip to associate clients with, index of X-FWD IP. + ] +} +``` diff --git a/registry/djarbz/modules/copyparty/copyparty.tftest.hcl b/registry/djarbz/modules/copyparty/copyparty.tftest.hcl new file mode 100644 index 000000000..e6fab8439 --- /dev/null +++ b/registry/djarbz/modules/copyparty/copyparty.tftest.hcl @@ -0,0 +1,181 @@ +# --- Test Case 1: Required Variables --- +run "plan_with_required_vars" { + command = plan + + variables { + agent_id = "example-agent-id" + } +} + +# --- Test Case 2: Coder App URL uses custom port --- +run "app_url_uses_port" { + command = plan + + variables { + agent_id = "example-agent-id" + port = 19999 + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:19999" + error_message = "Expected copyparty app URL to include configured port" + } +} + +# --- Test Case 3: Default Values --- +run "test_defaults" { + # This run block applies the module with default values + # (except for the required 'agent_id' provided above). + + variables { + agent_id = "example-agent-id" + } + + # --- Asserts for coder_app "copyparty" --- + assert { + condition = resource.coder_app.copyparty.display_name == "copyparty" + error_message = "Default display_name is incorrect" + } + + assert { + condition = resource.coder_app.copyparty.slug == "copyparty" + error_message = "Default slug is incorrect" + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:3923" + error_message = "Default URL is incorrect, expected port 3923" + } + + assert { + condition = resource.coder_app.copyparty.subdomain == false + error_message = "Default subdomain should be false" + } + + assert { + condition = resource.coder_app.copyparty.share == "owner" + error_message = "Default share value should be 'owner'" + } + + assert { + condition = resource.coder_app.copyparty.open_in == "slim-window" + error_message = "Default open_in value should be 'slim-window'" + } + + # --- Asserts for coder_script "copyparty" --- + assert { + condition = coder_script.copyparty.display_name == "copyparty" + error_message = "Script display_name is incorrect" + } + + # Check rendered script content (this assumes your run.sh uses the variables) + assert { + condition = strcontains(coder_script.copyparty.script, "PORT=\"3923\"") + error_message = "Script content does not reflect default port" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/tmp/copyparty.log\"") + error_message = "Script content does not reflect default log_path" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"\"") + error_message = "Script content does not reflect default empty arguments" + } +} + +# --- Test Case 4: Custom Values --- +run "test_custom_values" { + # Override default variables for this specific run + variables { + agent_id = "example-agent-id" + port = 8080 + slug = "my-custom-app" + display_name = "My Custom App" + share = "authenticated" + open_in = "tab" + pinned_version = "v1.2.3" + arguments = ["--verbose", "-v"] + log_path = "/var/log/custom.log" + } + + # --- Asserts for coder_app "copyparty" --- + assert { + condition = resource.coder_app.copyparty.display_name == "My Custom App" + error_message = "Custom display_name was not applied" + } + + assert { + condition = resource.coder_app.copyparty.slug == "my-custom-app" + error_message = "Custom slug was not applied" + } + + assert { + condition = resource.coder_app.copyparty.url == "http://localhost:8080" + error_message = "Custom port was not applied to URL" + } + + assert { + condition = resource.coder_app.copyparty.share == "authenticated" + error_message = "Custom share value was not applied" + } + + assert { + condition = resource.coder_app.copyparty.open_in == "tab" + error_message = "Custom open_in value was not applied" + } + + # --- Asserts for coder_script "copyparty" --- + assert { + condition = strcontains(coder_script.copyparty.script, "PORT=\"8080\"") + error_message = "Script content does not reflect custom port" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "PINNED_VERSION=\"v1.2.3\"") + error_message = "Script content does not reflect custom pinned_version" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "IFS=',' read -r -a ARGUMENTS \u003c\u003c\u003c \"--verbose,-v\"") + error_message = "Script content does not reflect custom arguments" + } + + assert { + condition = strcontains(coder_script.copyparty.script, "LOG_PATH=\"/var/log/custom.log\"") + error_message = "Script content does not reflect custom log_path" + } +} + +# --- Test Case 5: Validation Failure (open_in) --- +run "test_invalid_open_in" { + # This is a 'plan' test that expects a failure + command = plan + + variables { + agent_id = "example-agent-id" + open_in = "invalid-value" + } + + # Expect this plan to fail due to the validation rule in 'var.open_in' + expect_failures = [ + var.open_in, + ] +} + +# --- Test Case 6: Validation Failure (share) --- +run "test_invalid_share" { + # This is a 'plan' test that expects a failure + command = plan + + variables { + agent_id = "example-agent-id" + share = "everyone" # This is not 'owner', 'authenticated', or 'public' + } + + # Expect this plan to fail due to the validation rule in 'var.share' + expect_failures = [ + var.share, + ] +} diff --git a/registry/djarbz/modules/copyparty/main.tf b/registry/djarbz/modules/copyparty/main.tf new file mode 100644 index 000000000..822387c1e --- /dev/null +++ b/registry/djarbz/modules/copyparty/main.tf @@ -0,0 +1,174 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + coder = { + source = "coder/coder" + version = ">= 2.5" + } + } +} + +locals { + # A built-in icon like "/icon/code.svg" or a full URL of icon + icon_url = "/icon/copyparty.svg" + # a map of all possible values + # options = { + # "Option 1" = { + # "name" = "Option 1", + # "value" = "1" + # "icon" = "/emojis/1.png" + # } + # "Option 2" = { + # "name" = "Option 2", + # "value" = "2" + # "icon" = "/emojis/2.png" + # } + # } +} + +# Add required variables for your modules and remove any unneeded variables +variable "agent_id" { + type = string + description = "The ID of a Coder agent." +} + +variable "log_path" { + type = string + description = "The path to log copyparty to." + default = "/tmp/copyparty.log" +} + +variable "port" { + type = number + description = "ports to listen on (comma/range); ignored for unix-sockets (default: 3923)" + default = 3923 +} + +variable "slug" { + type = string + description = "The slug for the copyparty application." + default = "copyparty" +} + +variable "display_name" { + type = string + description = "The display name for the copyparty application." + default = "copyparty" +} + +variable "group" { + type = string + description = "The name of a group that this app belongs to." + default = null +} + +variable "open_in" { + type = string + description = <<-EOT + Determines where the app will be opened. Valid values are `"tab"` and `"slim-window" (default)`. + `"tab"` opens in a new tab in the same browser window. + `"slim-window"` opens a new browser window without navigation controls. + EOT + default = "slim-window" + validation { + condition = contains(["tab", "slim-window"], var.open_in) + error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'." + } +} + +variable "subdomain" { + type = bool + description = <<-EOT + Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. + If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. + EOT + default = false +} + +variable "share" { + type = string + default = "owner" + validation { + condition = var.share == "owner" || var.share == "authenticated" || var.share == "public" + error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'." + } +} + +# variable "mutable" { +# type = bool +# description = "Whether the parameter is mutable." +# default = true +# } + +variable "order" { + type = number + description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order)." + default = null +} +# Add other variables here + +variable "pinned_version" { + type = string + description = "Install a specific version in semver format (v1.19.16)." + default = "" +} + +variable "arguments" { + type = list(string) + description = "A list of arguments to pass to the application." + default = [] +} + + +resource "coder_script" "copyparty" { + agent_id = var.agent_id + display_name = "copyparty" + icon = local.icon_url + script = templatefile("${path.module}/run.sh", { + LOG_PATH : var.log_path, + PORT : var.port, + PINNED_VERSION : var.pinned_version, + ARGUMENTS : join(",", var.arguments), + }) + run_on_start = true + run_on_stop = false +} + +resource "coder_app" "copyparty" { + agent_id = var.agent_id + slug = var.slug + display_name = var.display_name + url = "http://localhost:${var.port}" + icon = local.icon_url + subdomain = var.subdomain + share = var.share + order = var.order + group = var.group + open_in = var.open_in + + # Remove if the app does not have a healthcheck endpoint + healthcheck { + url = "http://localhost:${var.port}" + interval = 5 + threshold = 6 + } +} + +# data "coder_parameter" "copyparty" { +# type = "list(string)" +# name = "copyparty" +# display_name = "copyparty" +# icon = local.icon_url +# mutable = var.mutable +# default = local.options["Option 1"]["value"] + +# dynamic "option" { +# for_each = local.options +# content { +# icon = option.value.icon +# name = option.value.name +# value = option.value.value +# } +# } +# } diff --git a/registry/djarbz/modules/copyparty/run.sh b/registry/djarbz/modules/copyparty/run.sh new file mode 100755 index 000000000..a138f5402 --- /dev/null +++ b/registry/djarbz/modules/copyparty/run.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash + +# Convert templated variables to shell variables +# This variable is assigned to itself, so the assignment does nothing. +# shellcheck disable=SC2269 +LOG_PATH="${LOG_PATH}" + +# Ports to listen on (comma/range); ignored for unix-sockets (default: 3923) +PORT="${PORT}" +# Pinned version (e.g., v1.19.16); overrides latest release discovery if set +PINNED_VERSION="${PINNED_VERSION}" +# Custom CLI Arguments# The variable from Terraform is a single, comma-separated string. +# We need to split it into a proper bash array using the comma (,) as the delimiter. +IFS=',' read -r -a ARGUMENTS <<< "${ARGUMENTS}" + +# VARIABLE appears unused. Verify use (or export if used externally). +# shellcheck disable=SC2034 +MODULE_NAME="Copyparty" + +# VARIABLE appears unused. Verify use (or export if used externally). +# shellcheck disable=SC2034 +BOLD='\033[0;1m' + +printf '%sInstalling %s ...\n\n' "$${BOLD}" "$${MODULE_NAME}" + +# Add code here +# Use variables from the templatefile function in main.tf +# e.g. LOG_PATH, PORT, etc. + +printf "🐍 Verifying Python 3 installation...\n" +if ! command -v python3 &> /dev/null; then + printf "❌ Python3 could not be found. Please install it to continue.\n" + exit 1 +fi +printf "✅ Python3 is installed.\n\n" + +RELEASE_TO_INSTALL="" +# Install provided version to pin, otherwise discover latest github release from `https://github.com/9001/copyparty`. +if [[ -n "$${PINNED_VERSION}" ]]; then + printf "📌 Pinned version specified: %s\n" "$${PINNED_VERSION}" + # Verify that it is in v#.#.# format + if [[ ! "$${PINNED_VERSION}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + printf "❌ Invalid format for PINNED_VERSION. Expected 'v#.#.#' (e.g., v1.19.16).\n" + exit 1 + fi + RELEASE_TO_INSTALL="$${PINNED_VERSION}" + printf "✅ Using pinned version %s.\n\n" "$${RELEASE_TO_INSTALL}" +else + printf "🔎 Discovering latest release from GitHub...\n" + # Use curl to get the latest release tag from the GitHub API and sed to parse it + LATEST_RELEASE=$(curl -fsSL https://api.github.com/repos/9001/copyparty/releases/latest | grep '"tag_name":' | sed -E 's/.*"(v[^"]+)".*/\1/') + if [[ -z "$${LATEST_RELEASE}" ]]; then + printf "❌ Could not determine the latest release. Please check your internet connection.\n" + exit 1 + fi + RELEASE_TO_INSTALL="$${LATEST_RELEASE}" + printf "🏷️ Latest release is %s.\n\n" "$${RELEASE_TO_INSTALL}" +fi + +# Download appropriate release version assets: `copyparty-sfx.py` and `helptext.html`. +printf "🚀 Downloading copyparty v%s...\n" "$${RELEASE_TO_INSTALL}" +DOWNLOAD_URL="https://github.com/9001/copyparty/releases/download/$${RELEASE_TO_INSTALL}" + +printf "⏬ Downloading copyparty-sfx.py...\n" +if ! curl -fsSL -o /tmp/copyparty-sfx.py "$${DOWNLOAD_URL}/copyparty-sfx.py"; then + printf "❌ Failed to download copyparty-sfx.py.\n" + exit 1 +fi + +printf "⏬ Downloading helptext.html...\n" +if ! curl -fsSL -o /tmp/helptext.html "$${DOWNLOAD_URL}/helptext.html"; then + # This is not a fatal error, just a warning. + printf "⚠️ Could not download helptext.html. The application will still work.\n" +fi + +chmod +x /tmp/copyparty-sfx.py +printf "✅ Download complete.\n\n" + +printf "🥳 Installation complete!\n\n" + +# Build a clean, quoted string of the command for logging purposes only. +log_command="python3 /tmp/copyparty-sfx.py -p '$${PORT}'" +for arg in "$${ARGUMENTS[@]}"; do + # printf "DEBUG: ARG [$${arg}]\n" + log_command+=" '$${arg}'" +done + +# Clear the log file and write the header and command string using printf. +{ + printf "=== Starting copyparty at %s ===\n" "$(date)" + printf "EXECUTING: %s\n" "$${log_command}" +} > "$${LOG_PATH}" + +printf "👷 Starting %s in background...\n\n" "$${MODULE_NAME}" + +# Execute the actual command using the robust array expansion. +# Then, append its output (stdout and stderr) to the log file. +python3 /tmp/copyparty-sfx.py -p "$${PORT}" "$${ARGUMENTS[@]}" >> "$${LOG_PATH}" 2>&1 & + +printf "✅ Service started. Check logs at %s\n\n" "$${LOG_PATH}"