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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/pages/terminalBackup/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Terminal backup page
* @param {() => void} onclose
*/
export default function TerminalBackup(onclose) {
import("./terminalBackup").then((res) => res.default(onclose));
}
150 changes: 150 additions & 0 deletions src/pages/terminalBackup/terminalBackup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import settingsPage from "components/settingsPage";
import toast from "components/toast";
import alert from "dialogs/alert";
import loader from "dialogs/loader";
import FileBrowser from "pages/fileBrowser";
import helpers from "utils/helpers";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unused import

helpers is imported but never referenced anywhere in this file.

Suggested change
import helpers from "utils/helpers";

(Remove the import entirely.)


export default function TerminalBackup(onclose) {
const backupSettings = {
alpineBase: true,
packages: true,
home: false,
public: false,
};

const items = [
{
key: "alpineBase",
text: "Alpine",
checkbox: backupSettings.alpineBase,
info: strings["alpineBase info"] || "Include Alpine environment",
},
/*{
key: "packages",
text: strings["packages"] || "Packages",
checkbox: backupSettings.packages,
info: strings["packages info"] || "Include installed packages",
},*/
{
key: "home",
text: strings["home"] || "Home",
checkbox: backupSettings.home,
info:
strings["home info"] ||
"Include Alpine /home /root and /public directory",
},
{
key: "backup",
text: strings["backup"],
chevron: false,
info: "Create backup with selected components",
},
];

const page = settingsPage(strings["backup"], items, callback, undefined, {
preserveOrder: true,
pageClassName: "detail-settings-page",
listClassName: "detail-settings-list",
infoAsDescription: true,
valueInTail: true,
});

const oldHide = page.hide;
page.hide = () => {
oldHide();
onclose?.();
};
page.show();

function setPackagesTooltip() {
const packagesRow = document.querySelector('[data-key="packages"]');
if (!packagesRow) return;
const input = packagesRow.querySelector(".input-checkbox input");
if (input) {
input.disabled = !!backupSettings.alpineBase;
}
}
Comment on lines +60 to +67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Functions reference a commented-out UI element

The packages settings item is disabled in the items array (wrapped in a block comment), so these helper functions will always have a null reference for the row. setPackagesTooltip() returns early immediately, enforcePackageRule() never updates the checkbox UI, and the matching case in callback is never triggered.

Also, backupSettings.public is declared in the initial state object but never consumed by any item or callback — it can be removed.


function enforcePackageRule(value) {
if (backupSettings.alpineBase && value === false) {
toast(
strings["packages cannot be disabled when alpine base enabled"] ||
"Packages cannot be disabled while Alpine base is enabled.",
);
backupSettings.packages = true;
const pkgRow = document.querySelector('[data-key="packages"]');
if (pkgRow) {
const checkbox = pkgRow.querySelector(".input-checkbox input");
if (checkbox) checkbox.checked = true;
}
return true;
}
return false;
}

async function performBackup() {
try {
const { url } = await FileBrowser("folder", strings["select folder"]);

if (!url) return;

loader.showTitleLoader(
strings["creating backup"] || "Creating backup...",
);

const backupPath = await Terminal.backup({
alpineBase: backupSettings.alpineBase,
packages: backupSettings.packages,
home: backupSettings.home,
});

await system.copyToUri(
backupPath,
url,
"aterm_backup.tar",
console.log,
console.error,
);

loader.removeTitleLoader();
alert(
strings.success.toUpperCase(),
`${strings["backup successful"] || "Backup successful"}.`,
);
} catch (error) {
loader.removeTitleLoader();
console.error("Terminal backup failed:", error);
toast(error.toString());
}
}

function callback(key, value) {
switch (key) {
case "alpineBase":
backupSettings.alpineBase = value;
if (value) {
backupSettings.packages = true;
const pkgRow = document.querySelector('[data-key="packages"]');
if (pkgRow) {
const checkbox = pkgRow.querySelector(".input-checkbox input");
if (checkbox) checkbox.checked = true;
}
}
setPackagesTooltip();
break;
case "packages":
if (enforcePackageRule(value)) return;
backupSettings.packages = value;
break;
case "home":
backupSettings.home = value;
break;
case "backup":
performBackup();
return;
default:
break;
}
}
}
3 changes: 3 additions & 0 deletions src/plugins/terminal/scripts/init-sandbox.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ ARGS="$ARGS -b /dev/urandom:/dev/random"
ARGS="$ARGS -b /proc"
ARGS="$ARGS -b /sys"
ARGS="$ARGS -b $PREFIX"
#keeping /public for backward compatibility, as some users might be using it for storing files
ARGS="$ARGS -b $PREFIX/public:/public"
ARGS="$ARGS -b $PREFIX/public:/home"
ARGS="$ARGS -b $PREFIX/public:/root"
Comment on lines +67 to +68
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 /home and /root bound to the same directory

Both /home and /root inside the Alpine container now point to $PREFIX/public. While this is consistent with the backup's handling of the home option (which only archives public), it means any process writing to /home/<user> or /root shares the same underlying directory. This can cause unexpected collisions — for example, if a package creates /root/.bashrc and a user's home directory is also /home/root, they'll overwrite each other's dotfiles.

Consider whether the intent is truly to unify these paths, and add a comment explaining the reasoning if so.

ARGS="$ARGS -b $PREFIX/alpine/tmp:/dev/shm"


Expand Down
71 changes: 66 additions & 5 deletions src/plugins/terminal/www/Terminal.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,25 +321,86 @@ const Terminal = {
* console.error(`Backup failed: ${error}`);
* }
*/
backup() {
backup(options = {}) {
return new Promise(async (resolve, reject) => {
if (!await this.isInstalled()) {
reject("Alpine is not installed.");
return;
}

const opts = {
alpineBase: true,
packages: false,
home: false,
...options,
};

const includeFiles = [];
const excludePaths = [
"alpine/system",
"alpine/vendor",
"alpine/sdcard",
"alpine/storage",
"alpine/apex",
"alpine/odm",
"alpine/product",
"alpine/system_ext",
"alpine/linkerconfig",
"alpine/proc",
"alpine/sys",
"alpine/dev",
"alpine/run",
"alpine/tmp",
];

if (opts.alpineBase) {
includeFiles.push("alpine", ".downloaded", ".extracted", ".configured", "axs");
if (opts.packages) {
includeFiles.push("alpine/data");
excludePaths.splice(excludePaths.indexOf("alpine/data"), 1);
}
Comment on lines +358 to +361
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 alpine/data missing from excludePaths, causing wrong element to be spliced

"alpine/data" is not present in the excludePaths array, so excludePaths.indexOf("alpine/data") returns -1. Calling excludePaths.splice(-1, 1) then removes the last element — "alpine/tmp" — instead of "alpine/data". This results in two unintended behaviors:

  1. When packages is false, alpine/data is never excluded from the tar archive (user-installed packages are always included).
  2. When packages is true, alpine/tmp is not excluded, so temporary files end up in the backup.

The fix is to add "alpine/data" to excludePaths so the splice call removes the correct entry:

Suggested change
if (opts.packages) {
includeFiles.push("alpine/data");
excludePaths.splice(excludePaths.indexOf("alpine/data"), 1);
}
if (opts.packages) {
includeFiles.push("alpine/data");
excludePaths.splice(excludePaths.indexOf("alpine/data"), 1);

And add "alpine/data" to the excludePaths array:

const excludePaths = [
    "alpine/data",   // <-- add this
    "alpine/system",
    ...
    "alpine/tmp",
];

if (opts.home) {
const checkCmd = `test -e "$PREFIX/public" && echo "exists" || echo "not"`;
const checkResult = await Executor.execute(checkCmd);
if (checkResult === "exists") {
includeFiles.push("public");
}
}
} else {
// If alpineBase is disabled, only include public if home is enabled
if (opts.home) {
const checkCmd = `test -e "$PREFIX/public" && echo "exists" || echo "not"`;
const checkResult = await Executor.execute(checkCmd);
if (checkResult === "exists") {
includeFiles.push("public");
}
}
}

if (!includeFiles.length) {
reject("No components selected for backup.");
return;
}

let excludeCmd = "";
excludePaths.forEach((path) => {
excludeCmd += ` --exclude=${path}`;
});

const includeStr = includeFiles.join(" ");
const cmd = `
set -e
INCLUDE_FILES="alpine .downloaded .extracted .configured axs"
INCLUDE_FILES="${includeStr}"
if [ "$FDROID" = "true" ]; then
INCLUDE_FILES="$INCLUDE_FILES libtalloc.so.2 libproot-xed.so"
fi
EXCLUDE="--exclude=alpine/data --exclude=alpine/system --exclude=alpine/vendor --exclude=alpine/sdcard --exclude=alpine/storage --exclude=alpine/public --exclude=alpine/apex --exclude=alpine/odm --exclude=alpine/product --exclude=alpine/system_ext --exclude=alpine/linkerconfig --exclude=alpine/proc --exclude=alpine/sys --exclude=alpine/dev --exclude=alpine/run --exclude=alpine/tmp"
tar -cf "$PREFIX/aterm_backup.tar" -C "$PREFIX" $EXCLUDE $INCLUDE_FILES
tar -cf "$PREFIX/aterm_backup.tar" -C "$PREFIX"${excludeCmd} $INCLUDE_FILES
echo "ok"
`;

const result = await Executor.execute(cmd);
if (result === "ok") {
resolve(cordova.file.dataDirectory + "aterm_backup.tar");
resolve(cordova.file.dataDirectory + "usr/aterm_backup.tar");
} else {
reject(result);
}
Expand Down
98 changes: 34 additions & 64 deletions src/settings/terminalSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import loader from "dialogs/loader";
import fonts from "lib/fonts";
import appSettings from "lib/settings";
import FileBrowser from "pages/fileBrowser";
import TerminalBackup from "pages/terminalBackup";
import helpers from "utils/helpers";

export default function terminalSettings() {
Expand Down Expand Up @@ -248,7 +249,7 @@ export default function terminalSettings() {

return;
case "backup":
terminalBackup();
TerminalBackup(() => console.log("Backup page closed"));
return;

case "restore":
Expand Down Expand Up @@ -291,74 +292,43 @@ export default function terminalSettings() {
break;
}
}
}

/**
* Creates a backup of the terminal installation
*/
async function terminalBackup() {
try {
// Ask user to select backup location
const { url } = await FileBrowser("folder", strings["select folder"]);

loader.showTitleLoader();

// Create backup
const backupPath = await Terminal.backup();
await system.copyToUri(
backupPath,
url,
"aterm_backup.tar",
console.log,
console.error,
);
loader.removeTitleLoader();
alert(strings.success.toUpperCase(), `${strings["backup successful"]}.`);
} catch (error) {
loader.removeTitleLoader();
console.error("Terminal backup failed:", error);
toast(error.toString());
}
}

/**
* Restores terminal installation
*/
async function terminalRestore() {
try {
await Executor.execute("rm -rf $PREFIX/aterm_backup.*");
/**
* Restores terminal installation
*/
async function terminalRestore() {
try {
await Executor.execute("rm -rf $PREFIX/aterm_backup.*");

sdcard.openDocumentFile(
async (data) => {
loader.showTitleLoader();
//this will create a file at $PREFIX/atem_backup.tar.tar
await system.copyToUri(
data.uri,
cordova.file.dataDirectory,
"aterm_backup.tar",
console.log,
console.error,
);
sdcard.openDocumentFile(
async (data) => {
loader.showTitleLoader();
//this will create a file at $PREFIX/atem_backup.tar.tar
await system.copyToUri(
data.uri,
cordova.file.dataDirectory,
"aterm_backup.tar",
console.log,
console.error,
);

// Restore
await Terminal.restore();
// Restore
await Terminal.restore();

//Cleanup restore file
await Executor.execute("rm -rf $PREFIX/aterm_backup.*");
//Cleanup restore file
await Executor.execute("rm -rf $PREFIX/aterm_backup.*");

loader.removeTitleLoader();
alert(
strings.success.toUpperCase(),
"Terminal restored successfully",
);
},
toast,
"application/x-tar",
);
} catch (error) {
loader.removeTitleLoader();
console.error("Terminal restore failed:", error);
toast(error.toString());
}
loader.removeTitleLoader();
alert(strings.success.toUpperCase(), "Terminal restored successfully");
},
toast,
"application/x-tar",
);
} catch (error) {
loader.removeTitleLoader();
console.error("Terminal restore failed:", error);
toast(error.toString());
}
}

Expand Down