Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2ac5646
Implement drive mount using DocumentsProvider
ianopolous Jun 12, 2026
3ed4227
Support seeking in drive mount.
ianopolous Jun 19, 2026
8d6c187
Keep alive during streaming with foreground service
ianopolous Jun 19, 2026
cac304a
Support random access writes in drive mount
ianopolous Jun 19, 2026
6dc38b7
Better error msg in sync
ianopolous Jun 27, 2026
60d304a
Add "Download logs" menu item, and include sync messages and errors i…
ianopolous Jun 27, 2026
bfdda52
Support syncing on mobile data
ianopolous Jun 27, 2026
058be46
Add missing file
ianopolous Jun 27, 2026
3647511
Make sync use a FGS if starting by user, which should run to completi…
ianopolous Jun 27, 2026
05f8584
Fix cas exceptions in initial sync
ianopolous Jun 27, 2026
8dc80c4
Fix unclosed streams
ianopolous Jun 27, 2026
e21dc8c
Support sync status and log per pair
ianopolous Jun 28, 2026
04b66b2
Fix mobile data sync in background for existing users
ianopolous Jun 29, 2026
ace289c
Update Peergos jar
ianopolous Jun 30, 2026
bce6115
Add android CI tests using emulator
ianopolous Jun 30, 2026
995338f
Fix CI?
ianopolous Jun 30, 2026
a73a6ba
Fix CI on x86?
ianopolous Jun 30, 2026
75924d7
More CI logs
ianopolous Jun 30, 2026
9c6558d
Run peergos server for tests outside emulator
ianopolous Jun 30, 2026
99390b1
Fix CI
ianopolous Jun 30, 2026
b45ab8b
Fix CI allowing http to localhost
ianopolous Jun 30, 2026
dd3902f
Add large dir sync test to CI
ianopolous Jun 30, 2026
030de7e
Test thumbnails and hashes in sync
ianopolous Jun 30, 2026
43d0642
Reduce test time
ianopolous Jun 30, 2026
9326dfb
Fix sync on self hosted servers
ianopolous Jul 1, 2026
9a011f7
Fix boundary handling in treehash calculation
ianopolous Jul 10, 2026
d786c94
Fix latent bug in seek during sync partial file upload
ianopolous Jul 10, 2026
8fa311a
Fix sync truncate
ianopolous Jul 10, 2026
6b1b12b
Fix short reads in sync
ianopolous Jul 10, 2026
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
136 changes: 136 additions & 0 deletions .github/workflows/android.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: Android sync
on: [push]
jobs:
e2e:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
api-level: [30, 35]
steps:
- uses: actions/checkout@v4
with:
submodules: recursive

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21

- name: Install Ant
run: sudo apt-get update && sudo apt-get install -y ant

- name: Build host Peergos fat jar
run: cd web-ui && ant dist

- name: Enable KVM
run: |
if [ ! -e /dev/kvm ]; then
echo "::error::/dev/kvm not present on this runner"
exit 1
fi
sudo chmod 666 /dev/kvm
ls -la /dev/kvm

- name: Start Peergos server on host
run: |
export PEERGOS_HOST_DIR=$(mktemp -d)
nohup java -jar web-ui/server/Peergos.jar pki-init \
-port 8000 \
-proxy-target /ip4/127.0.0.1/tcp/5556 \
-gateway-port 8090 \
-ipfs-api-address /ip4/127.0.0.1/tcp/5001 \
-ipfs-gateway-address /ip4/127.0.0.1/tcp/8080 \
-ipfs-swarm-port 4001 \
-ipfs.metrics.port 6001 \
-useIPFS false \
-listen-host 0.0.0.0 \
-admin-usernames peergos \
-logToConsole true \
-enable-gc false \
-default-quota 10737418240 \
-quota-upload-limit-seconds 1 \
max-users 10000 \
max-daily-signups 20000 \
PEERGOS_PATH "$PEERGOS_HOST_DIR" \
peergos.password testpassword \
pki.keygen.password testpkipassword \
pki.keyfile.password testpassword > peergos-host.log 2>&1 &
echo $! > peergos-host.pid
echo "Waiting for server to come up..."
for i in $(seq 1 180); do
if ! kill -0 $(cat peergos-host.pid) 2>/dev/null; then
echo "::error::peergos server exited early"
tail -50 peergos-host.log
exit 1
fi
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 2 http://localhost:8000/ || echo 000)
if [ "$code" != "000" ]; then
echo "Server ready (HTTP $code) after ${i}s"
exit 0
fi
sleep 1
done
echo "::error::peergos server did not respond on :8000 within 180s"
tail -80 peergos-host.log
exit 1

- uses: gradle/actions/setup-gradle@v4

- name: AVD cache
uses: actions/cache@v4
id: avd-cache
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-${{ matrix.api-level }}

- name: Create AVD and generate snapshot for caching
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
target: ${{ matrix.api-level == 30 && 'google_apis' || 'default' }}
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -partition-size 8192
disable-animations: false
script: echo "Generated AVD snapshot for caching."

- name: Run instrumented tests
uses: reactivecircus/android-emulator-runner@v2
timeout-minutes: 60
with:
api-level: ${{ matrix.api-level }}
arch: x86_64
target: ${{ matrix.api-level == 30 && 'google_apis' || 'default' }}
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none -partition-size 8192
disable-animations: true
script: |
adb logcat -c
adb logcat -v threadtime > logcat.txt &
LOGCAT_PID=$!
set +e
./gradlew connectedDebugAndroidTest --info -Pandroid.testInstrumentationRunnerArguments.peergosHost=10.0.2.2 -Pandroid.testInstrumentationRunnerArguments.peergosPort=8000
STATUS=$?
kill $LOGCAT_PID 2>/dev/null || true
exit $STATUS

- name: Stop Peergos server
if: always()
run: |
if [ -f peergos-host.pid ]; then
kill $(cat peergos-host.pid) 2>/dev/null || true
fi

- if: always()
uses: actions/upload-artifact@v4
with:
name: android-test-reports-api${{ matrix.api-level }}
path: |
app/build/reports/androidTests/connected/**
app/build/outputs/androidTest-results/connected/**
logcat.txt
peergos-host.log
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "web-ui"]
path = web-ui
url = https://github.com/peergos/web-ui
21 changes: 19 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ android {
applicationId = "peergos.android"
minSdk = 30
targetSdk = 35
versionCode = 51
versionName = "1.29.2"
versionCode = 55
versionName = "1.29.7"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
Expand Down Expand Up @@ -41,6 +41,20 @@ android {
version = "3.22.1"
}
}
packaging {
resources {
excludes += setOf(
"META-INF/*.SF",
"META-INF/*.DSA",
"META-INF/*.RSA",
"META-INF/NOTICE*",
"META-INF/LICENSE*",
"META-INF/DEPENDENCIES",
"META-INF/INDEX.LIST",
"META-INF/MANIFEST.MF"
)
}
}
}

dependencies {
Expand All @@ -60,6 +74,9 @@ dependencies {
implementation("androidx.core:core-ktx:1.16.0")
implementation("com.yubico.yubikit:android:3.0.1")
implementation("com.yubico.yubikit:fido:3.0.1")
implementation("androidx.security:security-crypto:1.1.0") {
exclude(group = "com.google.code.gson", module = "gson")
}
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
Expand Down
Binary file modified app/libs/Peergos.jar
Binary file not shown.
12 changes: 12 additions & 0 deletions app/src/androidTest/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<application>
<provider
android:name="peergos.android.TestDocumentsProvider"
android:authorities="peergos.android.test.documents"
android:exported="true"
android:grantUriPermissions="true" />
</application>

</manifest>
78 changes: 78 additions & 0 deletions app/src/androidTest/java/peergos/android/AndroidTruncateTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package peergos.android;

import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.provider.DocumentsContract;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.ByteArrayInputStream;
import java.nio.file.Path;
import java.util.Optional;
import java.util.Random;

import peergos.server.Main;
import peergos.shared.Crypto;
import peergos.shared.user.fs.ResumeUploadProps;

/**
* Regression test for AndroidSyncFileSystem.truncate.
*
* truncate opens the file descriptor to shrink it; the mode string must be a real
* ParcelFileDescriptor mode. A bare "t" is rejected by ParcelFileDescriptor.parseMode
* (a mode must start with r/w), so truncate throws and shrink-syncs to an Android
* device fail — the local file keeps its stale trailing bytes. This drives truncate
* through the same SAF path the sync uses (DocumentFile over TestDocumentsProvider).
*/
@RunWith(AndroidJUnit4.class)
public class AndroidTruncateTest {

private Context ctx;
private Crypto crypto;
private AndroidSyncFileSystem fs;

@Before
public void setUp() {
ctx = InstrumentationRegistry.getInstrumentation().getTargetContext();
crypto = Main.initCrypto(new ScryptAndroid());

// Wipe the provider's root (count=0 seeds nothing).
Uri authorityUri = Uri.parse("content://" + TestDocumentsProvider.AUTHORITY);
Bundle seed = new Bundle();
seed.putInt("count", 0);
seed.putLong("seed", 1);
seed.putLong("minSize", 0);
seed.putLong("maxSize", 0);
ctx.getContentResolver().call(authorityUri, TestDocumentsProvider.METHOD_RESET_AND_SEED, null, seed);

Uri treeUri = DocumentsContract.buildTreeDocumentUri(
TestDocumentsProvider.AUTHORITY, TestDocumentsProvider.ROOT_ID);
fs = new AndroidSyncFileSystem(treeUri, ctx, crypto);
}

@Test
public void truncateShrinksFile() throws Exception {
int size = 200_000;
byte[] data = new byte[size];
new Random(42).nextBytes(data);

Path p = fs.resolve("doc.bin");
fs.setBytes(p, 0,
new AndroidAsyncReader(new ByteArrayInputStream(data), () -> new ByteArrayInputStream(data)),
size, Optional.empty(), Optional.empty(), Optional.empty(),
ResumeUploadProps.random(crypto), () -> false, s -> {});
Assert.assertEquals(size, fs.size(p));

long newSize = size / 2;
fs.truncate(p, newSize); // throws on the current code (invalid mode "t")

Assert.assertEquals(newSize, fs.size(p));
}
}
Loading
Loading