feat: add archive module (#422)
This change adds a new `archive` module to the Coder registry. It can be used to archive user-data from pre-defined locations and restore it as well. Here we also explore: - A new method of passing arrays from Terraform to Bash - A new method of writing Bash scripts that minimizes the interaction with terraform interpolation - Extensive test-suite that not only tests that Terraform options can be selected, but also the resulting script behaviors --------- Co-authored-by: Cian Johnston <cian@coder.com> Co-authored-by: DevCats <christofer@coder.com>
This commit is contained in:
parent
ca7bc42946
commit
e34320cb0b
1
.icons/folder.svg
Normal file
1
.icons/folder.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48" fill="#FFF"><path d="M7.05 40q-1.2 0-2.1-.925-.9-.925-.9-2.075V11q0-1.15.9-2.075Q5.85 8 7.05 8h14l3 3h17q1.15 0 2.075.925.925.925.925 2.075v23q0 1.15-.925 2.075Q42.2 40 41.05 40Zm0-29v26h34V14H22.8l-3-3H7.05Zm0 0v26Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 289 B |
163
registry/coder-labs/modules/archive/README.md
Normal file
163
registry/coder-labs/modules/archive/README.md
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
---
|
||||||
|
display_name: Archive
|
||||||
|
description: Create automated and user-invocable scripts that archive and extract selected files/directories with optional compression (gzip or zstd).
|
||||||
|
icon: ../../../../.icons/folder.svg
|
||||||
|
verified: false
|
||||||
|
tags: [backup, archive, tar, helper]
|
||||||
|
---
|
||||||
|
|
||||||
|
# Archive
|
||||||
|
|
||||||
|
This module installs small, robust scripts in your workspace to create and extract tar archives from a list of files and directories. It supports optional compression (gzip or zstd). The create command prints only the resulting archive path to stdout; operational logs go to stderr. An optional stop hook can also create an archive automatically when the workspace stops, and an optional start hook can wait for an archive on-disk and extract it on start.
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "archive" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder-labs/archive/coder"
|
||||||
|
version = "0.0.1"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
|
||||||
|
paths = ["./projects", "./code"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Installs two commands into the workspace `$PATH`: `coder-archive-create` and `coder-archive-extract`.
|
||||||
|
- Creates a single `.tar`, `.tar.gz`, or `.tar.zst` containing selected paths (depends on `tar`).
|
||||||
|
- Optional compression: `gzip`, `zstd` (depends on `gzip` or `zstd`).
|
||||||
|
- Stores defaults so commands can be run without arguments (supports overriding via CLI flags).
|
||||||
|
- Logs and status messages go to stderr, the create command prints only the final archive path to stdout.
|
||||||
|
- Optional:
|
||||||
|
- `create_on_stop` to create an archive automatically when the workspace stops.
|
||||||
|
- `extract_on_start` to wait for an archive to appear and extract it on start.
|
||||||
|
|
||||||
|
> [!WARNING]
|
||||||
|
> The `create_on_stop` feature uses the `coder_script` `run_on_stop` which may not work as expected on certain templates without additional provider configuration. The agent may be terminated before the script completes. See [coder/coder#6174](https://github.com/coder/coder/issues/6174) for provider-specific workarounds and [coder/coder#6175](https://github.com/coder/coder/issues/6175) for tracking a fix.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Basic example:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "archive" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder-labs/archive/coder"
|
||||||
|
version = "0.0.1"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
|
||||||
|
# Paths to include in the archive (files or directories).
|
||||||
|
directory = "~"
|
||||||
|
paths = [
|
||||||
|
"./projects",
|
||||||
|
"./code",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Customize compression and output:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "archive" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder-labs/archive/coder"
|
||||||
|
version = "0.0.1"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
|
||||||
|
directory = "/"
|
||||||
|
paths = ["/etc", "/home"]
|
||||||
|
compression = "zstd" # "gzip" | "zstd" | "none"
|
||||||
|
output_dir = "/tmp/backup" # defaults to /tmp
|
||||||
|
archive_name = "my-backup" # base name (extension is inferred from compression)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Enable auto-archive on stop:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "archive" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder-labs/archive/coder"
|
||||||
|
version = "0.0.1"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
|
||||||
|
# Creates /tmp/coder-archive.tar.gz of the users home directory (defaults).
|
||||||
|
create_on_stop = true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract on start:
|
||||||
|
|
||||||
|
```tf
|
||||||
|
module "archive" {
|
||||||
|
count = data.coder_workspace.me.start_count
|
||||||
|
source = "registry.coder.com/coder-labs/archive/coder"
|
||||||
|
version = "0.0.1"
|
||||||
|
agent_id = coder_agent.example.id
|
||||||
|
|
||||||
|
# Where to look for the archive file to extract:
|
||||||
|
output_dir = "/tmp"
|
||||||
|
archive_name = "my-archive"
|
||||||
|
compression = "gzip"
|
||||||
|
|
||||||
|
# Waits up to 5 minutes for /tmp/my-archive.tar.gz to be present, note that
|
||||||
|
# using a long timeout will delay every workspace start by this much until the
|
||||||
|
# archive is present.
|
||||||
|
extract_on_start = true
|
||||||
|
extract_wait_timeout_seconds = 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Command usage
|
||||||
|
|
||||||
|
The installer writes the following files:
|
||||||
|
|
||||||
|
- `$CODER_SCRIPT_DATA_DIR/archive-lib.sh`
|
||||||
|
- `$CODER_SCRIPT_BIN_DIR/coder-archive-create`
|
||||||
|
- `$CODER_SCRIPT_BIN_DIR/coder-archive-extract`
|
||||||
|
|
||||||
|
Create usage:
|
||||||
|
|
||||||
|
```console
|
||||||
|
coder-archive-create [OPTIONS] [PATHS...]
|
||||||
|
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
|
||||||
|
-C, --directory <DIRECTORY> Change to directory for archiving (default from module)
|
||||||
|
-f, --file <ARCHIVE> Output archive file (default from module)
|
||||||
|
-h, --help Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract usage:
|
||||||
|
|
||||||
|
```console
|
||||||
|
coder-archive-extract [OPTIONS]
|
||||||
|
-c, --compression <gzip|zstd|none> Compression algorithm (default from module)
|
||||||
|
-C, --directory <DIRECTORY> Extract into directory (default from module)
|
||||||
|
-f, --file <ARCHIVE> Archive file to extract (default from module)
|
||||||
|
-h, --help Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Use Terraform defaults:
|
||||||
|
|
||||||
|
```
|
||||||
|
coder-archive-create
|
||||||
|
```
|
||||||
|
|
||||||
|
- Override compression and output file at runtime:
|
||||||
|
|
||||||
|
```
|
||||||
|
coder-archive-create --compression zstd --file /tmp/backups/archive.tar.zst
|
||||||
|
```
|
||||||
|
|
||||||
|
- Add extra paths on the fly (in addition to the Terraform defaults):
|
||||||
|
|
||||||
|
```
|
||||||
|
coder-archive-create /etc/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
- Extract an archive into a directory:
|
||||||
|
|
||||||
|
```
|
||||||
|
coder-archive-extract --file /tmp/backups/archive.tar.gz --directory /tmp/restore
|
||||||
|
```
|
||||||
33
registry/coder-labs/modules/archive/archive.tftest.hcl
Normal file
33
registry/coder-labs/modules/archive/archive.tftest.hcl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
mock_provider "coder" {}
|
||||||
|
|
||||||
|
run "apply_defaults" {
|
||||||
|
command = apply
|
||||||
|
|
||||||
|
variables {
|
||||||
|
agent_id = "agent-123"
|
||||||
|
paths = ["~/project", "/etc/hosts"]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
condition = output.archive_path == "/tmp/coder-archive.tar.gz"
|
||||||
|
error_message = "archive_path should be empty when archive_name is not set"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run "apply_with_name" {
|
||||||
|
command = apply
|
||||||
|
|
||||||
|
variables {
|
||||||
|
agent_id = "agent-123"
|
||||||
|
paths = ["/etc/hosts"]
|
||||||
|
archive_name = "nightly"
|
||||||
|
output_dir = "/tmp/backups"
|
||||||
|
compression = "zstd"
|
||||||
|
create_archive_on_stop = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {
|
||||||
|
condition = output.archive_path == "/tmp/backups/nightly.tar.zst"
|
||||||
|
error_message = "archive_path should be computed from archive_name + output_dir + extension"
|
||||||
|
}
|
||||||
|
}
|
||||||
348
registry/coder-labs/modules/archive/main.test.ts
Normal file
348
registry/coder-labs/modules/archive/main.test.ts
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
import { describe, expect, it, beforeAll } from "bun:test";
|
||||||
|
import {
|
||||||
|
execContainer,
|
||||||
|
findResourceInstance,
|
||||||
|
runContainer,
|
||||||
|
runTerraformApply,
|
||||||
|
runTerraformInit,
|
||||||
|
testRequiredVariables,
|
||||||
|
type TerraformState,
|
||||||
|
} from "~test";
|
||||||
|
|
||||||
|
const USE_XTRACE =
|
||||||
|
process.env.ARCHIVE_TEST_XTRACE === "1" || process.env.XTRACE === "1";
|
||||||
|
|
||||||
|
const IMAGE = "alpine";
|
||||||
|
const BIN_DIR = "/tmp/coder-script-data/bin";
|
||||||
|
const DATA_DIR = "/tmp/coder-script-data";
|
||||||
|
|
||||||
|
type ExecResult = {
|
||||||
|
exitCode: number;
|
||||||
|
stdout: string;
|
||||||
|
stderr: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureRunOk = (label: string, res: ExecResult) => {
|
||||||
|
if (res.exitCode !== 0) {
|
||||||
|
console.error(
|
||||||
|
`[${label}] non-zero exit code: ${res.exitCode}\n--- stdout ---\n${res.stdout.trim()}\n--- stderr ---\n${res.stderr.trim()}\n--------------`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(res.exitCode).toBe(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const sh = async (id: string, cmd: string): Promise<ExecResult> => {
|
||||||
|
const res = await execContainer(id, ["sh", "-c", cmd]);
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bashRun = async (id: string, cmd: string): Promise<ExecResult> => {
|
||||||
|
const injected = USE_XTRACE ? `/bin/bash -x ${cmd}` : cmd;
|
||||||
|
return sh(id, injected);
|
||||||
|
};
|
||||||
|
|
||||||
|
const prepareContainer = async (image = IMAGE) => {
|
||||||
|
const id = await runContainer(image);
|
||||||
|
// Prepare script dirs and deps.
|
||||||
|
ensureRunOk(
|
||||||
|
"mkdirs",
|
||||||
|
await sh(id, `mkdir -p ${BIN_DIR} ${DATA_DIR} /tmp/backup`),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Install tools used by tests.
|
||||||
|
ensureRunOk(
|
||||||
|
"apk add",
|
||||||
|
await sh(id, "apk add --no-cache bash tar gzip zstd coreutils"),
|
||||||
|
);
|
||||||
|
|
||||||
|
return id;
|
||||||
|
};
|
||||||
|
|
||||||
|
const installArchive = async (
|
||||||
|
state: TerraformState,
|
||||||
|
opts?: { env?: string[] },
|
||||||
|
) => {
|
||||||
|
const instance = findResourceInstance(state, "coder_script");
|
||||||
|
const id = await prepareContainer();
|
||||||
|
// Run installer script with correct env for CODER_SCRIPT paths.
|
||||||
|
const args = ["bash"];
|
||||||
|
if (USE_XTRACE) args.push("-x");
|
||||||
|
args.push("-c", instance.script);
|
||||||
|
|
||||||
|
const resp = await execContainer(id, args, [
|
||||||
|
"--env",
|
||||||
|
`CODER_SCRIPT_BIN_DIR=${BIN_DIR}`,
|
||||||
|
"--env",
|
||||||
|
`CODER_SCRIPT_DATA_DIR=${DATA_DIR}`,
|
||||||
|
...(opts?.env ?? []),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
install: {
|
||||||
|
exitCode: resp.exitCode,
|
||||||
|
stdout: resp.stdout.trim(),
|
||||||
|
stderr: resp.stderr.trim(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileExists = async (id: string, path: string) => {
|
||||||
|
const res = await sh(id, `test -f ${path} && echo yes || echo no`);
|
||||||
|
return res.stdout.trim() === "yes";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isExecutable = async (id: string, path: string) => {
|
||||||
|
const res = await sh(id, `test -x ${path} && echo yes || echo no`);
|
||||||
|
return res.stdout.trim() === "yes";
|
||||||
|
};
|
||||||
|
|
||||||
|
const listTar = async (id: string, path: string) => {
|
||||||
|
// Try to autodetect compression flags from extension.
|
||||||
|
let cmd = "";
|
||||||
|
if (path.endsWith(".tar.gz")) {
|
||||||
|
cmd = `tar -tzf ${path}`;
|
||||||
|
} else if (path.endsWith(".tar.zst")) {
|
||||||
|
// validate with zstd and ask tar to list via --zstd.
|
||||||
|
cmd = `zstd -t -q ${path} && tar --zstd -tf ${path}`;
|
||||||
|
} else {
|
||||||
|
cmd = `tar -tf ${path}`;
|
||||||
|
}
|
||||||
|
return sh(id, cmd);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("archive", () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await runTerraformInit(import.meta.dir);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure required variables are enforced.
|
||||||
|
testRequiredVariables(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
});
|
||||||
|
|
||||||
|
it("installs wrapper scripts to BIN_DIR and library to DATA_DIR", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
});
|
||||||
|
|
||||||
|
// The Terraform output should reflect defaults from main.tf.
|
||||||
|
expect(state.outputs.archive_path.value).toEqual(
|
||||||
|
"/tmp/coder-archive.tar.gz",
|
||||||
|
);
|
||||||
|
|
||||||
|
const { id, install } = await installArchive(state);
|
||||||
|
ensureRunOk("install", install);
|
||||||
|
|
||||||
|
expect(install.stdout).toContain(
|
||||||
|
`Installed archive library to: ${DATA_DIR}/archive-lib.sh`,
|
||||||
|
);
|
||||||
|
expect(install.stdout).toContain(
|
||||||
|
`Installed create script to: ${BIN_DIR}/coder-archive-create`,
|
||||||
|
);
|
||||||
|
expect(install.stdout).toContain(
|
||||||
|
`Installed extract script to: ${BIN_DIR}/coder-archive-extract`,
|
||||||
|
);
|
||||||
|
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-create`)).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
expect(await isExecutable(id, `${BIN_DIR}/coder-archive-extract`)).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses sane defaults: creates gzip archive at the default path and logs to stderr", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
// Keep defaults: compression=gzip, output_dir=/tmp, archive_name=coder-archive.
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await installArchive(state);
|
||||||
|
|
||||||
|
const createTestdata = await bashRun(
|
||||||
|
id,
|
||||||
|
`mkdir ~/gzip; touch ~/gzip/defaults.txt`,
|
||||||
|
);
|
||||||
|
ensureRunOk("create testdata", createTestdata);
|
||||||
|
|
||||||
|
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
|
||||||
|
ensureRunOk("archive-create default run", run);
|
||||||
|
|
||||||
|
// Only the archive path should print to stdout.
|
||||||
|
expect(run.stdout.trim()).toEqual("/tmp/coder-archive.tar.gz");
|
||||||
|
expect(await fileExists(id, "/tmp/coder-archive.tar.gz")).toBe(true);
|
||||||
|
|
||||||
|
// Some useful diagnostics should be on stderr.
|
||||||
|
expect(run.stderr).toContain("Creating archive:");
|
||||||
|
expect(run.stderr).toContain("Compression: gzip");
|
||||||
|
|
||||||
|
const list = await listTar(id, "/tmp/coder-archive.tar.gz");
|
||||||
|
ensureRunOk("list default archive", list);
|
||||||
|
expect(list.stdout).toContain("gzip/defaults.txt");
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
it("creates a gzip archive with explicit -f and includes extra CLI paths", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
// Provide a simple default path so we can assert contents.
|
||||||
|
paths: `["~/gzip"]`,
|
||||||
|
compression: "gzip",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await installArchive(state);
|
||||||
|
|
||||||
|
const createTestdata = await bashRun(
|
||||||
|
id,
|
||||||
|
`mkdir ~/gzip; touch ~/gzip/test.txt; touch ~/gziptest.txt`,
|
||||||
|
);
|
||||||
|
ensureRunOk("create testdata", createTestdata);
|
||||||
|
|
||||||
|
const out = "/tmp/backup/test-archive.tar.gz";
|
||||||
|
const run = await bashRun(
|
||||||
|
id,
|
||||||
|
`${BIN_DIR}/coder-archive-create -f ${out} ~/gziptest.txt`,
|
||||||
|
);
|
||||||
|
ensureRunOk("archive-create gzip explicit -f", run);
|
||||||
|
|
||||||
|
expect(run.stdout.trim()).toEqual(out);
|
||||||
|
expect(await fileExists(id, out)).toBe(true);
|
||||||
|
|
||||||
|
const list = await sh(id, `tar -tzf ${out}`);
|
||||||
|
ensureRunOk("tar -tzf contents (gzip)", list);
|
||||||
|
expect(list.stdout).toContain("gzip/test.txt");
|
||||||
|
expect(list.stdout).toContain("gziptest.txt");
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
it("creates a zstd-compressed archive when requested via CLI override", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
paths: `["/etc/hostname"]`,
|
||||||
|
// Module default is gzip, override at runtime to zstd.
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await installArchive(state);
|
||||||
|
|
||||||
|
const out = "/tmp/backup/zstd-archive.tar.zst";
|
||||||
|
const run = await bashRun(
|
||||||
|
id,
|
||||||
|
`${BIN_DIR}/coder-archive-create --compression zstd -f ${out}`,
|
||||||
|
);
|
||||||
|
ensureRunOk("archive-create zstd", run);
|
||||||
|
|
||||||
|
expect(run.stdout.trim()).toEqual(out);
|
||||||
|
|
||||||
|
// Check integrity via zstd and that tar can list it.
|
||||||
|
ensureRunOk("zstd -t", await sh(id, `test -f ${out} && zstd -t -q ${out}`));
|
||||||
|
ensureRunOk("tar --zstd -tf", await sh(id, `tar --zstd -tf ${out}`));
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
it("creates an uncompressed tar when compression=none", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
// Keep module defaults but override at runtime.
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await installArchive(state);
|
||||||
|
|
||||||
|
const out = "/tmp/backup/raw-archive.tar";
|
||||||
|
const run = await bashRun(
|
||||||
|
id,
|
||||||
|
`${BIN_DIR}/coder-archive-create --compression none -f ${out}`,
|
||||||
|
);
|
||||||
|
ensureRunOk("archive-create none", run);
|
||||||
|
|
||||||
|
expect(run.stdout.trim()).toEqual(out);
|
||||||
|
ensureRunOk("tar -tf (none)", await sh(id, `tar -tf ${out} >/dev/null`));
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
it("applies exclude patterns from Terraform", async () => {
|
||||||
|
// Include a file, but also exclude it via Terraform defaults to ensure
|
||||||
|
// exclusion flows through.
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
paths: `["/etc/hostname"]`,
|
||||||
|
exclude_patterns: `["/etc/hostname"]`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await installArchive(state);
|
||||||
|
|
||||||
|
const out = "/tmp/backup/excluded.tar.gz";
|
||||||
|
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create -f ${out}`);
|
||||||
|
ensureRunOk("archive-create with exclude_patterns", run);
|
||||||
|
|
||||||
|
const list = await sh(id, `tar -tzf ${out}`);
|
||||||
|
ensureRunOk("tar -tzf contents (exclude)", list);
|
||||||
|
expect(list.stdout).not.toContain("etc/hostname"); // Excluded by Terraform default.
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
it("adds a run_on_stop script when enabled", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
create_on_stop: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const coderScripts = state.resources.filter(
|
||||||
|
(r) => r.type === "coder_script",
|
||||||
|
);
|
||||||
|
// Installer (run_on_start) + run_on_stop.
|
||||||
|
expect(coderScripts.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("extracts a previously created archive into a target directory", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
paths: `["/etc/hostname"]`,
|
||||||
|
compression: "gzip",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await installArchive(state);
|
||||||
|
|
||||||
|
// Create archive.
|
||||||
|
const out = "/tmp/backup/extract-test.tar.gz";
|
||||||
|
const created = await bashRun(
|
||||||
|
id,
|
||||||
|
`${BIN_DIR}/coder-archive-create -f ${out} /etc/hosts`,
|
||||||
|
);
|
||||||
|
ensureRunOk("create for extract", created);
|
||||||
|
|
||||||
|
// Extract archive.
|
||||||
|
const extractDir = "/tmp/extract";
|
||||||
|
const extract = await bashRun(
|
||||||
|
id,
|
||||||
|
`${BIN_DIR}/coder-archive-extract -f ${out} -C ${extractDir}`,
|
||||||
|
);
|
||||||
|
ensureRunOk("archive-extract", extract);
|
||||||
|
|
||||||
|
// Verify a known file exists after extraction.
|
||||||
|
const exists = await sh(
|
||||||
|
id,
|
||||||
|
`test -f ${extractDir}/etc/hosts && echo ok || echo no`,
|
||||||
|
);
|
||||||
|
expect(exists.stdout.trim()).toEqual("ok");
|
||||||
|
}, 20000);
|
||||||
|
|
||||||
|
it("honors Terraform defaults without CLI args (compression, name, output_dir)", async () => {
|
||||||
|
const state = await runTerraformApply(import.meta.dir, {
|
||||||
|
agent_id: "agent-123",
|
||||||
|
compression: "zstd",
|
||||||
|
archive_name: "my-default",
|
||||||
|
output_dir: "/tmp/defout",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await installArchive(state);
|
||||||
|
|
||||||
|
const run = await bashRun(id, `${BIN_DIR}/coder-archive-create`);
|
||||||
|
ensureRunOk("archive-create terraform defaults", run);
|
||||||
|
expect(run.stdout.trim()).toEqual("/tmp/defout/my-default.tar.zst");
|
||||||
|
expect(run.stderr).toContain("Creating archive:");
|
||||||
|
expect(run.stderr).toContain("Compression: zstd");
|
||||||
|
ensureRunOk(
|
||||||
|
"zstd -t",
|
||||||
|
await sh(id, "zstd -t -q /tmp/defout/my-default.tar.zst"),
|
||||||
|
);
|
||||||
|
ensureRunOk(
|
||||||
|
"tar --zstd -tf",
|
||||||
|
await sh(id, "tar --zstd -tf /tmp/defout/my-default.tar.zst"),
|
||||||
|
);
|
||||||
|
}, 30000);
|
||||||
|
});
|
||||||
134
registry/coder-labs/modules/archive/main.tf
Normal file
134
registry/coder-labs/modules/archive/main.tf
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.0"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
coder = {
|
||||||
|
source = "coder/coder"
|
||||||
|
version = ">= 0.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "agent_id" {
|
||||||
|
description = "The ID of a Coder agent."
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "paths" {
|
||||||
|
description = "List of files/directories to include in the archive. Defaults to the current directory."
|
||||||
|
type = list(string)
|
||||||
|
default = ["."]
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "exclude_patterns" {
|
||||||
|
description = "Exclude patterns for the archive."
|
||||||
|
type = list(string)
|
||||||
|
default = []
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "compression" {
|
||||||
|
description = "Compression algorithm for the archive. Supported: gzip, zstd, none."
|
||||||
|
type = string
|
||||||
|
default = "gzip"
|
||||||
|
validation {
|
||||||
|
condition = contains(["gzip", "zstd", "none"], var.compression)
|
||||||
|
error_message = "compression must be one of: gzip, zstd, none."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "archive_name" {
|
||||||
|
description = "Optional archive base name without extension. If empty, defaults to \"coder-archive\"."
|
||||||
|
type = string
|
||||||
|
default = "coder-archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "output_dir" {
|
||||||
|
description = "Optional output directory where the archive will be written. Defaults to \"/tmp\"."
|
||||||
|
type = string
|
||||||
|
default = "/tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "directory" {
|
||||||
|
description = "Change current directory to this path before creating or extracting the archive. Defaults to the user's home directory."
|
||||||
|
type = string
|
||||||
|
default = "~"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "create_on_stop" {
|
||||||
|
description = "If true, also create a run_on_stop script that creates the archive automatically on workspace stop."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "extract_on_start" {
|
||||||
|
description = "If true, the installer will wait for an archive and extract it on start."
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "extract_wait_timeout_seconds" {
|
||||||
|
description = "Timeout (seconds) to wait for an archive when extract_on_start is true."
|
||||||
|
type = number
|
||||||
|
default = 5
|
||||||
|
}
|
||||||
|
|
||||||
|
# Provide a stable script filename and sensible defaults.
|
||||||
|
locals {
|
||||||
|
extension = var.compression == "gzip" ? ".tar.gz" : var.compression == "zstd" ? ".tar.zst" : ".tar"
|
||||||
|
|
||||||
|
# Ensure ~ is expanded because it cannot be expanded inside quotes in a
|
||||||
|
# templated shell script.
|
||||||
|
paths = [for v in var.paths : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
|
||||||
|
exclude_patterns = [for v in var.exclude_patterns : replace(v, "/^~(\\/|$)/", "$$HOME$1")]
|
||||||
|
directory = replace(var.directory, "/^~(\\/|$)/", "$$HOME$1")
|
||||||
|
output_dir = replace(var.output_dir, "/^~(\\/|$)/", "$$HOME$1")
|
||||||
|
|
||||||
|
archive_path = "${local.output_dir}/${var.archive_name}${local.extension}"
|
||||||
|
}
|
||||||
|
|
||||||
|
output "archive_path" {
|
||||||
|
description = "Full path to the archive file that will be created, extracted, or both."
|
||||||
|
value = local.archive_path
|
||||||
|
}
|
||||||
|
|
||||||
|
# This script installs the user-facing archive script into $CODER_SCRIPT_BIN_DIR.
|
||||||
|
# The installed script can be run manually by the user to create an archive.
|
||||||
|
resource "coder_script" "archive_start_script" {
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "Archive"
|
||||||
|
icon = "/icon/folder.svg"
|
||||||
|
run_on_start = true
|
||||||
|
start_blocks_login = var.extract_on_start
|
||||||
|
|
||||||
|
# Render the user-facing archive script with Terraform defaults, then write it to $CODER_SCRIPT_BIN_DIR
|
||||||
|
script = templatefile("${path.module}/run.sh", {
|
||||||
|
TF_LIB_B64 = base64encode(file("${path.module}/scripts/archive-lib.sh")),
|
||||||
|
TF_PATHS = join(" ", formatlist("%q", local.paths)),
|
||||||
|
TF_EXCLUDE_PATTERNS = join(" ", formatlist("%q", local.exclude_patterns)),
|
||||||
|
TF_COMPRESSION = var.compression,
|
||||||
|
TF_ARCHIVE_PATH = local.archive_path,
|
||||||
|
TF_DIRECTORY = local.directory,
|
||||||
|
TF_EXTRACT_ON_START = var.extract_on_start,
|
||||||
|
TF_EXTRACT_WAIT_TIMEOUT = var.extract_wait_timeout_seconds,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
# Optionally, also register a run_on_stop script that creates the archive automatically
|
||||||
|
# when the workspace stops. It simply invokes the installed archive script.
|
||||||
|
resource "coder_script" "archive_stop_script" {
|
||||||
|
count = var.create_on_stop ? 1 : 0
|
||||||
|
agent_id = var.agent_id
|
||||||
|
display_name = "Archive"
|
||||||
|
icon = "/icon/folder.svg"
|
||||||
|
run_on_stop = true
|
||||||
|
start_blocks_login = false
|
||||||
|
|
||||||
|
# Call the installed script. It will log to stderr and print the archive path to stdout.
|
||||||
|
# We redirect stdout to stderr to avoid surfacing the path in system logs if undesired.
|
||||||
|
# Remove the redirection if you want the path to appear in stdout on stop as well.
|
||||||
|
script = <<-EOT
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
"$CODER_SCRIPT_BIN_DIR/coder-archive-create"
|
||||||
|
EOT
|
||||||
|
}
|
||||||
75
registry/coder-labs/modules/archive/run.sh
Normal file
75
registry/coder-labs/modules/archive/run.sh
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
LIB_B64="${TF_LIB_B64}"
|
||||||
|
EXTRACT_ON_START="${TF_EXTRACT_ON_START}"
|
||||||
|
EXTRACT_WAIT_TIMEOUT="${TF_EXTRACT_WAIT_TIMEOUT}"
|
||||||
|
|
||||||
|
# Set script defaults from Terraform.
|
||||||
|
DEFAULT_PATHS=(${TF_PATHS})
|
||||||
|
DEFAULT_EXCLUDE_PATTERNS=(${TF_EXCLUDE_PATTERNS})
|
||||||
|
DEFAULT_COMPRESSION="${TF_COMPRESSION}"
|
||||||
|
DEFAULT_ARCHIVE_PATH="${TF_ARCHIVE_PATH}"
|
||||||
|
DEFAULT_DIRECTORY="${TF_DIRECTORY}"
|
||||||
|
|
||||||
|
# 1) Decode the library into $CODER_SCRIPT_DATA_DIR/archive-lib.sh (static, sourceable).
|
||||||
|
LIB_PATH="$CODER_SCRIPT_DATA_DIR/archive-lib.sh"
|
||||||
|
lib_tmp="$(mktemp -t coder-module-archive.XXXXXX))"
|
||||||
|
trap 'rm -f "$lib_tmp" 2>/dev/null || true' EXIT
|
||||||
|
|
||||||
|
# Decode the base64 content safely.
|
||||||
|
if ! printf '%s' "$LIB_B64" | base64 -d > "$lib_tmp"; then
|
||||||
|
echo "ERROR: Failed to decode archive library from base64." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
chmod 0644 "$lib_tmp"
|
||||||
|
mv "$lib_tmp" "$LIB_PATH"
|
||||||
|
|
||||||
|
# 2) Generate the wrapper scripts (create and extract).
|
||||||
|
create_wrapper() {
|
||||||
|
tmp="$(mktemp -t coder-module-archive.XXXXXX)"
|
||||||
|
trap 'rm -f "$tmp" 2>/dev/null || true' EXIT
|
||||||
|
cat > "$tmp" << EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
. "$LIB_PATH"
|
||||||
|
|
||||||
|
# Set defaults from Terraform (through installer).
|
||||||
|
$(
|
||||||
|
declare -p \
|
||||||
|
DEFAULT_PATHS \
|
||||||
|
DEFAULT_EXCLUDE_PATTERNS \
|
||||||
|
DEFAULT_COMPRESSION \
|
||||||
|
DEFAULT_ARCHIVE_PATH \
|
||||||
|
DEFAULT_DIRECTORY
|
||||||
|
)
|
||||||
|
|
||||||
|
$1 "\$@"
|
||||||
|
EOF
|
||||||
|
chmod 0755 "$tmp"
|
||||||
|
mv "$tmp" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
CREATE_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-create"
|
||||||
|
EXTRACT_WRAPPER_PATH="$CODER_SCRIPT_BIN_DIR/coder-archive-extract"
|
||||||
|
create_wrapper archive_create "$CREATE_WRAPPER_PATH"
|
||||||
|
create_wrapper archive_extract "$EXTRACT_WRAPPER_PATH"
|
||||||
|
|
||||||
|
echo "Installed archive library to: $LIB_PATH"
|
||||||
|
echo "Installed create script to: $CREATE_WRAPPER_PATH"
|
||||||
|
echo "Installed extract script to: $EXTRACT_WRAPPER_PATH"
|
||||||
|
|
||||||
|
# 3) Optionally wait for and extract an archive on start.
|
||||||
|
if [[ $EXTRACT_ON_START = true ]]; then
|
||||||
|
. "$LIB_PATH"
|
||||||
|
|
||||||
|
archive_wait_and_extract "$EXTRACT_WAIT_TIMEOUT" quiet || {
|
||||||
|
exit_code=$?
|
||||||
|
if [[ $exit_code -eq 2 ]]; then
|
||||||
|
echo "WARNING: Archive not found in backup path (this is expected with new workspaces)."
|
||||||
|
else
|
||||||
|
exit $exit_code
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
fi
|
||||||
279
registry/coder-labs/modules/archive/scripts/archive-lib.sh
Normal file
279
registry/coder-labs/modules/archive/scripts/archive-lib.sh
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '%s\n' "$@" >&2
|
||||||
|
}
|
||||||
|
warn() {
|
||||||
|
printf 'WARNING: %s\n' "$1" >&2
|
||||||
|
}
|
||||||
|
error() {
|
||||||
|
printf 'ERROR: %s\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
load_defaults() {
|
||||||
|
DEFAULT_PATHS=("${DEFAULT_PATHS[@]:-.}")
|
||||||
|
DEFAULT_EXCLUDE_PATTERNS=("${DEFAULT_EXCLUDE_PATTERNS[@]:-}")
|
||||||
|
DEFAULT_COMPRESSION="${DEFAULT_COMPRESSION:-gzip}"
|
||||||
|
DEFAULT_ARCHIVE_PATH="${DEFAULT_ARCHIVE_PATH:-/tmp/coder-archive.tar.gz}"
|
||||||
|
DEFAULT_DIRECTORY="${DEFAULT_DIRECTORY:-$HOME}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_tools() {
|
||||||
|
command -v tar > /dev/null 2>&1 || error "tar is required"
|
||||||
|
case "$1" in
|
||||||
|
gzip)
|
||||||
|
command -v gzip > /dev/null 2>&1 || error "gzip is required for gzip compression"
|
||||||
|
;;
|
||||||
|
zstd)
|
||||||
|
command -v zstd > /dev/null 2>&1 || error "zstd is required for zstd compression"
|
||||||
|
;;
|
||||||
|
none) ;;
|
||||||
|
*)
|
||||||
|
error "Unsupported compression algorithm: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
usage_archive_create() {
|
||||||
|
load_defaults
|
||||||
|
|
||||||
|
cat >&2 << USAGE
|
||||||
|
Usage: coder-archive-create [OPTIONS] [[PATHS] ...]
|
||||||
|
Options:
|
||||||
|
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
|
||||||
|
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
|
||||||
|
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
|
||||||
|
-h, --help Show this help
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_create() {
|
||||||
|
load_defaults
|
||||||
|
|
||||||
|
local compression="${DEFAULT_COMPRESSION}"
|
||||||
|
local directory="${DEFAULT_DIRECTORY}"
|
||||||
|
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||||
|
local paths=("${DEFAULT_PATHS[@]}")
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-c | --compression)
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
usage_archive_create
|
||||||
|
error "Missing value for $1"
|
||||||
|
fi
|
||||||
|
compression="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-C | --directory)
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
usage_archive_create
|
||||||
|
error "Missing value for $1"
|
||||||
|
fi
|
||||||
|
directory="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-f | --file)
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
usage_archive_create
|
||||||
|
error "Missing value for $1"
|
||||||
|
fi
|
||||||
|
file="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage_archive_create
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
paths+=("$1")
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
usage_archive_create
|
||||||
|
error "Unknown option: $1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
paths+=("$1")
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
ensure_tools "$compression"
|
||||||
|
|
||||||
|
local -a tar_opts=(-c -f "$file" -C "$directory")
|
||||||
|
case "$compression" in
|
||||||
|
gzip)
|
||||||
|
tar_opts+=(-z)
|
||||||
|
;;
|
||||||
|
zstd)
|
||||||
|
tar_opts+=(--zstd)
|
||||||
|
;;
|
||||||
|
none) ;;
|
||||||
|
*)
|
||||||
|
error "Unsupported compression algorithm: $compression"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
|
||||||
|
if [[ -n $path ]]; then
|
||||||
|
tar_opts+=(--exclude "$path")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Ensure destination directory exists.
|
||||||
|
dest="$(dirname "$file")"
|
||||||
|
mkdir -p "$dest" 2> /dev/null || error "Failed to create output dir: $dest"
|
||||||
|
|
||||||
|
log "Creating archive:"
|
||||||
|
log " Compression: $compression"
|
||||||
|
log " Directory: $directory"
|
||||||
|
log " Archive: $file"
|
||||||
|
log " Paths: ${paths[*]}"
|
||||||
|
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
tar "${tar_opts[@]}" "${paths[@]}"
|
||||||
|
|
||||||
|
printf '%s\n' "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
usage_archive_extract() {
|
||||||
|
load_defaults
|
||||||
|
|
||||||
|
cat >&2 << USAGE
|
||||||
|
Usage: coder-archive-extract [OPTIONS]
|
||||||
|
Options:
|
||||||
|
-c, --compression <gzip|zstd|none> Compression algorithm (default "${DEFAULT_COMPRESSION}")
|
||||||
|
-C, --directory <DIRECTORY> Change to directory (default "${DEFAULT_DIRECTORY}")
|
||||||
|
-f, --file <ARCHIVE> Output archive file (default "${DEFAULT_ARCHIVE_PATH}")
|
||||||
|
-h, --help Show this help
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_extract() {
|
||||||
|
load_defaults
|
||||||
|
|
||||||
|
local compression="${DEFAULT_COMPRESSION}"
|
||||||
|
local directory="${DEFAULT_DIRECTORY}"
|
||||||
|
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
-c | --compression)
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
usage_archive_extract
|
||||||
|
error "Missing value for $1"
|
||||||
|
fi
|
||||||
|
compression="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-C | --directory)
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
usage_archive_extract
|
||||||
|
error "Missing value for $1"
|
||||||
|
fi
|
||||||
|
directory="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-f | --file)
|
||||||
|
if [[ $# -lt 2 ]]; then
|
||||||
|
usage_archive_extract
|
||||||
|
error "Missing value for $1"
|
||||||
|
fi
|
||||||
|
file="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h | --help)
|
||||||
|
usage_archive_extract
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--)
|
||||||
|
shift
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
usage_archive_extract
|
||||||
|
error "Unknown option: $1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
ensure_tools "$compression"
|
||||||
|
|
||||||
|
local -a tar_opts=(-x -f "$file" -C "$directory")
|
||||||
|
case "$compression" in
|
||||||
|
gzip)
|
||||||
|
tar_opts+=(-z)
|
||||||
|
;;
|
||||||
|
zstd)
|
||||||
|
tar_opts+=(--zstd)
|
||||||
|
;;
|
||||||
|
none) ;;
|
||||||
|
*)
|
||||||
|
error "Unsupported compression algorithm: $compression"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for path in "${DEFAULT_EXCLUDE_PATTERNS[@]}"; do
|
||||||
|
if [[ -n $path ]]; then
|
||||||
|
tar_opts+=(--exclude "$path")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Ensure destination directory exists.
|
||||||
|
mkdir -p "$directory" || error "Failed to create directory: $directory"
|
||||||
|
|
||||||
|
log "Extracting archive:"
|
||||||
|
log " Compression: $compression"
|
||||||
|
log " Directory: $directory"
|
||||||
|
log " Archive: $file"
|
||||||
|
log " Exclude: ${DEFAULT_EXCLUDE_PATTERNS[*]}"
|
||||||
|
|
||||||
|
umask 077
|
||||||
|
tar "${tar_opts[@]}" "${paths[@]}"
|
||||||
|
|
||||||
|
printf 'Extracted %s into %s\n' "$file" "$directory"
|
||||||
|
}
|
||||||
|
|
||||||
|
archive_wait_and_extract() {
|
||||||
|
load_defaults
|
||||||
|
|
||||||
|
local timeout="${1:-300}"
|
||||||
|
local quiet="${2:-}"
|
||||||
|
local file="${DEFAULT_ARCHIVE_PATH}"
|
||||||
|
|
||||||
|
local start now
|
||||||
|
start=$(date +%s)
|
||||||
|
while true; do
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
archive_extract -f "$file"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ((timeout <= 0)); then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
now=$(date +%s)
|
||||||
|
if ((now - start >= timeout)); then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ -z $quiet ]]; then
|
||||||
|
printf 'ERROR: Timed out waiting for archive: %s\n' "$file" >&2
|
||||||
|
fi
|
||||||
|
return 2
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user