Skip to content

Commit 8464a33

Browse files
Merge branch 'main' of github.com:appdevforall/iiab-android into feat/K2GO-554-app-verify
2 parents 6bb7759 + 0de52c0 commit 8464a33

15 files changed

Lines changed: 1034 additions & 53 deletions

File tree

‎.github/workflows/prebuild-tests.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ jobs:
5757
npm run typecheck
5858
npm test
5959
60+
- name: box-identity tests
61+
working-directory: static/box-identity
62+
run: python -m unittest -v test_k2go_box_identity
63+
6064
# Fetch the real upstream maps role (the artifact the bake uses) and apply our in-flight
6165
# patches, so the contract test checks what will actually be baked, not a frozen fixture.
6266
# A patch that no longer applies (context drift) fails here, which is itself a useful signal.
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# Records that a box copy (Clone, Backup) does not carry (K2GO-549). One list for both.
2+
# Each line is a path or glob relative to the box root (installed-rootfs/iiab).
3+
# CopyExcludes anchors each line at the box root for rsync and for GNU tar.
4+
# Rules: "*" does not match "/". Never "**". Keep a dir that a service needs at start:
5+
# exclude its contents with "dir/*".
6+
# Database rows (sessions, peers, sync state) are removed on the receiver by k2go-box-identity.
7+
8+
# Kolibri. Logs hold the superuser password, client IPs and User-Agents.
9+
library/kolibri/logs/*.txt
10+
library/kolibri/logs/archive/*
11+
library/kolibri/process_cache/*/cache.db*
12+
library/kolibri/server.pid
13+
library/kolibri/process_control.flag
14+
library/kolibri/*.sqlite3-shm
15+
16+
# Calibre-Web
17+
var/log/calibre-web.log*
18+
usr/local/calibre-web-py3/access.log*
19+
usr/local/calibre-web-py3/calibre-web.log*
20+
library/calibre-web/xklb-metadata.db*
21+
var/log/xklb.log*
22+
library/downloads/calibre-web/*
23+
24+
# Forgejo. Keep library/forgejo/log: pdsm appends to it.
25+
library/forgejo/log/*.log*
26+
library/forgejo/data/repositories/*/*.git/logs
27+
library/forgejo/backups/*.zip
28+
library/forgejo/data/actions_log/*
29+
library/forgejo/data/forgejo.db-shm
30+
31+
# dash-node and maps. A copied job would restart the download of the other phone.
32+
library/dashboard/jobs.db
33+
library/dashboard/jobs.db-wal
34+
library/dashboard/jobs.db-shm
35+
library/dashboard/credentials.json.tmp-*
36+
var/log/dash-node.log*
37+
var/log/dash-rebuild.log*
38+
var/log/forgejo-seed.log*
39+
var/log/forgejo-refresh.log*
40+
var/log/code-addons-refresh.log*
41+
var/log/code-assets-refresh.log*
42+
library/downloads/maps/*
43+
library/www/maps/*.aria2
44+
library/www/maps/*.meta4
45+
46+
# conex-core
47+
var/log/conex-core.log*
48+
library/.conex-staging/*
49+
50+
# Web tier. Keep var/log/nginx: nginx does not start without it.
51+
var/log/nginx/*.log*
52+
var/log/php*-fpm.log*
53+
var/lib/php/sessions/sess_*
54+
var/log/uwsgi/app/*.log*
55+
var/lib/nginx/*/*
56+
library/working/*/*
57+
58+
# System logs and mail
59+
var/log/apt/*
60+
var/log/dpkg.log*
61+
var/log/alternatives.log*
62+
var/log/fontconfig.log*
63+
var/log/exim4/*
64+
var/log/wtmp.*
65+
var/log/btmp.*
66+
var/log/journal/*
67+
var/log/runit/*/*
68+
var/mail/*
69+
var/spool/exim4/input/*
70+
var/spool/exim4/msglog/*
71+
var/spool/exim4/db/*
72+
opt/iiab/iiab/iiab-install.log
73+
opt/iiab/iiab/iiab-debug.log
74+
root/.local/state/pipx/log/*
75+
76+
# Shell and tool history
77+
root/.bash_history
78+
root/.*_history
79+
root/.lesshst
80+
root/.viminfo
81+
root/.wget-hsts
82+
root/.local/share/nano/*
83+
root/.ssh/known_hosts*
84+
home/*/.bash_history
85+
home/*/.*_history
86+
home/*/.lesshst
87+
home/*/.viminfo
88+
home/*/.wget-hsts
89+
home/*/.local/share/nano/*
90+
home/*/.ssh/known_hosts*
91+
92+
# Per-box state that the box makes again
93+
root/.cache/aria2/dht.dat
94+
root/.cache/aria2/dht6.dat
95+
var/lib/libuuid/clock.txt
96+
97+
# Git reflogs. The clones stay.
98+
opt/*/.git/logs
99+
opt/*/.git/FETCH_HEAD
100+
opt/*/.git/ORIG_HEAD
101+
opt/iiab/*/.git/logs
102+
opt/iiab/*/.git/FETCH_HEAD
103+
opt/iiab/*/.git/ORIG_HEAD
104+
105+
# Runtime state. A stale pid makes a service stop the wrong process or skip its start.
106+
run/*.pid
107+
run/*/*.pid
108+
run/k2go/*
109+
var/lib/sudo/lectured/*
110+
tmp/*
111+
var/tmp/*
112+
# Core dumps. Not a bare "core": that matches the Kolibri core package.
113+
core.[0-9]*
114+
hs_err_pid*.log
115+
var/lib/systemd/coredump/*
116+
var/lib/systemd/pstore/*
117+
# Mount points (sdcard) are in CopyExcludes.MOUNT_POINTS, not here.

‎controller/app/src/main/java/org/appdevforall/k2go/PRootEngine.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.ArrayList;
2323
import java.util.List;
2424

25+
import org.appdevforall.k2go.boxidentity.data.DeviceIdStore;
2526
import org.appdevforall.k2go.network.data.FileResolvConfWriter;
2627
import org.appdevforall.k2go.network.data.PrefsDnsConfigRepository;
2728
import org.appdevforall.k2go.network.domain.ApplyDnsUseCase;
@@ -174,6 +175,15 @@ public void executeInContainer(Context context, String rootfsDir, String command
174175
args.add("-b");
175176
args.add(prootTmpPath + ":/tmp");
176177

178+
// K2GO-549: the box step k2go-box-identity reads it. Without it, the box keeps its values.
179+
try {
180+
File deviceId = DeviceIdStore.ensure(context.getNoBackupFilesDir());
181+
args.add("-b");
182+
args.add(deviceId.getCanonicalPath() + ":" + DeviceIdStore.GUEST_PATH);
183+
} catch (Exception idEx) {
184+
Log.w(TAG, "Device ID bind skipped: " + idEx.getMessage());
185+
}
186+
177187
// 4. Set Working Directory
178188
args.add("-w");
179189
args.add("/root");

‎controller/app/src/main/java/org/appdevforall/k2go/RsyncManager.java‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
import android.os.Looper;
1515
import android.util.Log;
1616

17+
import org.appdevforall.k2go.boxidentity.data.CopyExcludeFiles;
18+
import org.appdevforall.k2go.boxidentity.domain.CopyExcludes;
1719
import org.appdevforall.k2go.sync.domain.RsyncConfig;
1820
import org.appdevforall.k2go.sync.domain.RsyncOutcome;
1921
import org.appdevforall.k2go.sync.domain.RsyncProgress;
@@ -77,11 +79,16 @@ public boolean startServer(Context context, ShareConfig config, String pass, Str
7779
secretStore = new SecretStore(cacheDir);
7880
File secretsFile = secretStore.writeServerSecrets(config.user, pass);
7981

82+
// K2GO-549: the share is the box root. A copy never carries its records. An exception
83+
// here fails the start: no share without the list.
84+
File excludes = CopyExcludeFiles.rsync(context);
85+
8086
// PHASE 1 FIX: 'max connections = 3' protects the I/O bottleneck.
8187
// Config/argv assembly lives in the pure RsyncConfig domain (S14 step 1).
8288
String configContent = RsyncConfig.buildDaemonConf(
8389
pidFile.getAbsolutePath(), lockFile.getAbsolutePath(), config.rsyncPort,
84-
config.moduleName, dirToShare, config.user, secretsFile.getAbsolutePath());
90+
config.moduleName, dirToShare, config.user, secretsFile.getAbsolutePath(),
91+
excludes.getAbsolutePath());
8592

8693
writeTextToFile(configFile, configContent);
8794

@@ -128,8 +135,10 @@ public void startClient(Context context, ShareConfig config, String hostIp, int
128135

129136
String remoteUrl = RsyncConfig.buildRemoteUrl(user, hostIp, port, config.moduleName);
130137

131-
ProcessBuilder pb = new ProcessBuilder(RsyncConfig.clientArgs(
132-
rsyncBin.getAbsolutePath(), passFile.getAbsolutePath(), remoteUrl, destinationDir));
138+
// K2GO-549: also filter here, for a sender whose APK has no exclude list.
139+
ProcessBuilder pb = new ProcessBuilder(RsyncConfig.withExcludes(RsyncConfig.clientArgs(
140+
rsyncBin.getAbsolutePath(), passFile.getAbsolutePath(), remoteUrl, destinationDir),
141+
CopyExcludeFiles.rsync(context).getAbsolutePath(), CopyExcludes.rsyncProtectRules()));
133142

134143
pb.redirectErrorStream(true);
135144
rsyncProcess = pb.start();
@@ -232,7 +241,8 @@ else if (line.contains("@ERROR:") || line.startsWith("rsync:")
232241
+ (RsyncOutcome.isSuccess(exitCode) && exitCode != 0
233242
? " (non-zero treated as complete — see any [Rsync Output Error] lines above)" : ""));
234243
if (isCancelled) {
235-
mainHandler.post(() -> listener.onError(context.getString(R.string.rsync_error_cancelled)));
244+
// The cancel path already reset the screen (CloneFragment.onReceiveCancel).
245+
Log.i(TAG, "Rsync client stopped by the user");
236246
} else {
237247
switch (RsyncOutcome.classifyTransfer(exitCode)) {
238248
case COMPLETE:
@@ -251,6 +261,12 @@ else if (line.contains("@ERROR:") || line.startsWith("rsync:")
251261
}
252262

253263
} catch (Exception e) {
264+
// A user Cancel closes the stream under the reader. The cancel path already reset
265+
// the screen (CloneFragment.onReceiveCancel), so it is not an error.
266+
if (isCancelled) {
267+
Log.i(TAG, "Rsync client stopped by the user");
268+
return;
269+
}
254270
Log.e(TAG, "Rsync Client Exception", e);
255271
mainHandler.post(() -> listener.onError(context.getString(R.string.rsync_error_fatal, e.getMessage())));
256272
}
@@ -284,8 +300,9 @@ public void calculateTransferPlan(Context context, ShareConfig config, String ho
284300

285301
String remoteUrl = RsyncConfig.buildRemoteUrl(user, hostIp, port, config.moduleName);
286302

287-
ProcessBuilder pb = new ProcessBuilder(RsyncConfig.dryRunArgs(
288-
rsyncBin.getAbsolutePath(), passFile.getAbsolutePath(), remoteUrl, destinationDir));
303+
ProcessBuilder pb = new ProcessBuilder(RsyncConfig.withExcludes(RsyncConfig.dryRunArgs(
304+
rsyncBin.getAbsolutePath(), passFile.getAbsolutePath(), remoteUrl, destinationDir),
305+
CopyExcludeFiles.rsync(context).getAbsolutePath(), CopyExcludes.rsyncProtectRules()));
289306

290307
pb.redirectErrorStream(true);
291308
rsyncProcess = pb.start();

‎controller/app/src/main/java/org/appdevforall/k2go/backup/domain/BackupEngine.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
import android.content.Context;
1818
import android.util.Log;
1919

20+
import org.appdevforall.k2go.boxidentity.data.CopyExcludeFiles;
2021
import org.appdevforall.k2go.deploy.domain.ExtractProgress;
2122
import org.appdevforall.k2go.system.domain.TransferRate;
2223
import org.appdevforall.k2go.util.ProcessRunner;
2324

2425
import java.io.BufferedReader;
2526
import java.io.File;
27+
import java.io.IOException;
2628
import java.io.InputStream;
2729
import java.io.InputStreamReader;
2830
import java.io.OutputStream;
@@ -87,7 +89,17 @@ public static boolean streamBackup(Context ctx, OutputStream dest, ProgressListe
8789
// rootfs content, and are unreadable with the box stopped ("Cannot open: Permission denied"). Skipping
8890
// them keeps tar's exit honest (0) instead of failing the whole backup on a file that must not be in
8991
// it. The old "tar | gzip" pipe hit the same read error but gzip's exit=0 silently masked tar's exit=2.
92+
// K2GO-549: a backup never carries the box records. GNU tar applies --anchored and
93+
// --no-wildcards-match-slash only to the --exclude-from that follows them.
94+
File excludes;
95+
try {
96+
excludes = CopyExcludeFiles.tar(ctx, "installed-rootfs/iiab/");
97+
} catch (IOException e) {
98+
Log.e(TAG, "backup: exclude list not written", e);
99+
return false;
100+
}
90101
String cmd = "'" + tarBin + "' -cf - --ignore-failed-read "
102+
+ "--anchored --no-wildcards-match-slash --exclude-from='" + excludes.getAbsolutePath() + "' "
91103
+ (manifestArg != null ? manifestArg : "")
92104
+ "-C '" + iiabRootDir.getAbsolutePath() + "' installed-rootfs";
93105

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* ============================================================================
3+
* Name : CopyExcludeFiles.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : K2GO-560. Writes the box copy exclude list for rsync and tar.
7+
* ============================================================================
8+
*/
9+
package org.appdevforall.k2go.boxidentity.data;
10+
11+
import android.content.Context;
12+
13+
import org.appdevforall.k2go.boxidentity.domain.CopyExcludes;
14+
15+
import java.io.ByteArrayOutputStream;
16+
import java.io.File;
17+
import java.io.FileOutputStream;
18+
import java.io.IOException;
19+
import java.io.InputStream;
20+
import java.nio.charset.StandardCharsets;
21+
22+
/** Writes the {@link CopyExcludes} list to a file, in the form each copy tool reads. */
23+
public final class CopyExcludeFiles {
24+
25+
private CopyExcludeFiles() {
26+
}
27+
28+
/** For the rsync daemon that serves the box root (Clone send). */
29+
public static File rsync(Context ctx) throws IOException {
30+
return write(ctx, "box-copy-exclude.rsync", CopyExcludes.forRsync(asset(ctx)));
31+
}
32+
33+
/** For tar, where {@code memberPrefix} is the archive name of the box root (Backup). */
34+
public static File tar(Context ctx, String memberPrefix) throws IOException {
35+
return write(ctx, "box-copy-exclude.tar", CopyExcludes.forTar(asset(ctx), memberPrefix));
36+
}
37+
38+
private static String asset(Context ctx) throws IOException {
39+
try (InputStream in = ctx.getAssets().open(CopyExcludes.ASSET)) {
40+
ByteArrayOutputStream out = new ByteArrayOutputStream();
41+
byte[] buf = new byte[4096];
42+
int n;
43+
while ((n = in.read(buf)) > 0) out.write(buf, 0, n);
44+
return new String(out.toByteArray(), StandardCharsets.UTF_8);
45+
}
46+
}
47+
48+
private static File write(Context ctx, String name, String content) throws IOException {
49+
// Not the cache dir: Android can clear it while the Clone daemon still reads the file.
50+
File dir = ctx.getNoBackupFilesDir();
51+
File f = new File(dir, name);
52+
File tmp = new File(dir, name + ".tmp");
53+
try (FileOutputStream out = new FileOutputStream(tmp)) {
54+
out.write(content.getBytes(StandardCharsets.UTF_8));
55+
out.getFD().sync();
56+
}
57+
// The rsync daemon reads the file at each connection: replace it whole, never truncate it.
58+
// renameTo replaces an existing file on Android (POSIX rename).
59+
if (!tmp.renameTo(f)) {
60+
tmp.delete();
61+
throw new IOException("cannot write " + f);
62+
}
63+
return f;
64+
}
65+
}

0 commit comments

Comments
 (0)