diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a283d5c2..47418d23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,10 @@ name: Release +# One `v*` tag produces ONE GitHub release containing every platform: +# - server binaries + archives + checksums (GoReleaser) + GHCR docker images +# - desktop bundles: Linux .deb/.AppImage, Windows NSIS, macOS .dmg/.app.tar.gz +# - Android release APK + AAB (signed with ANDROID_KEYSTORE_* secrets when set) +# The desktop-* / android-v* tags still exist for platform-only releases. on: push: tags: ['v*'] @@ -9,21 +14,17 @@ permissions: packages: write jobs: - goreleaser: + server: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: 22 cache: yarn - cache-dependency-path: frontend/yarn.lock - uses: actions/setup-go@v5 with: go-version-file: go.mod - cache: true - uses: docker/setup-qemu-action@v3 - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 @@ -31,11 +32,120 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - uses: goreleaser/goreleaser-action@v6 + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 with: distribution: goreleaser version: '~> v2' args: release --clean env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NODE_OPTIONS: --max-old-space-size=8192 + + desktop: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: yarn + cache-dependency-path: desktop/ui/yarn.lock + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: desktop + + - name: Install Linux dependencies + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev libgtk-3-dev librsvg2-dev patchelf libfuse2 fuse3 + + - name: Install UI dependencies + working-directory: desktop/ui + run: yarn install --frozen-lockfile + + - name: Install tauri-cli + run: cargo install tauri-cli --version "^2" --locked + + - name: Build desktop bundles + working-directory: desktop + run: cargo tauri build + + - name: Publish bundles to GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + shell: bash + run: | + set -e + for _ in $(seq 1 90); do + gh release view "$TAG" >/dev/null 2>&1 && break + sleep 20 + done + gh release view "$TAG" >/dev/null + FILES=$(find desktop/target/release/bundle -type f \ + \( -name '*.deb' -o -name '*.AppImage' -o -name '*.msi' \ + -o -name '*.exe' -o -name '*.dmg' -o -name '*.app.tar.gz' \)) + [ -n "$FILES" ] + echo "$FILES" | while read -r f; do + name="cloudreve-desktop-$(basename "$f")" + cp "$f" "$name" + gh release upload "$TAG" "$name" --clobber + done + + android: + runs-on: ubuntu-latest + defaults: + run: + working-directory: android + env: + KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - uses: gradle/actions/setup-gradle@v4 + + - name: Decode release keystore + if: ${{ env.KEYSTORE_B64 != '' }} + run: | + echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/release.keystore" + { + echo "ANDROID_KEYSTORE_FILE=$RUNNER_TEMP/release.keystore" + echo "ANDROID_KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" + echo "ANDROID_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" + echo "ANDROID_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" + } >> "$GITHUB_ENV" + + - name: Build release APK + AAB + run: ./gradlew :app:assembleRelease :app:bundleRelease --console=plain + + - name: Publish APK + AAB to GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ github.ref_name }} + run: | + set -e + for _ in $(seq 1 90); do + gh release view "$TAG" >/dev/null 2>&1 && break + sleep 20 + done + gh release view "$TAG" >/dev/null + APK=$(find app/build/outputs/apk/release -name 'app-release*.apk' | head -1) + AAB=$(find app/build/outputs/bundle/release -name 'app-release*.aab' | head -1) + cp "$APK" "cloudreve-android-${TAG}.apk" + cp "$AAB" "cloudreve-android-${TAG}.aab" + gh release upload "$TAG" \ + "cloudreve-android-${TAG}.apk" \ + "cloudreve-android-${TAG}.aab" --clobber diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 00000000..4be91fe0 --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,42 @@ +# Privacy Policy — Cloudreve Mobile + +**Last updated: 2026-09-20** + +Cloudreve Mobile is an open-source client for self-hosted Cloudreve servers. It is designed so that your data stays between your device and the server you choose. + +## What the app does + +- Connects to a **Cloudreve server URL that you provide**. All file transfers, authentication, and API calls go directly between your device and that server. +- Stores your server address and session credentials **locally on your device** (Android DataStore / app-private storage). +- Uploads files, photos, or folders **only when you initiate it** (manual upload, share sheet, or a sync/camera-backup feature you explicitly enable). +- Optionally reads media files and folders you select for upload or sync features. + +## What the app does not do + +- No analytics, no telemetry, no crash reporting to us. +- No advertising, no tracking SDKs, no third-party data sharing. +- No data is sent to the developers of this app. There is no developer-operated backend — your configured server is the only remote endpoint. + +## Data safety summary + +| Category | Collected | Shared | +|---|---|---| +| Files and photos | Only files you choose to upload, sent to your own server | Never | +| Credentials | Stored on-device only | Never | +| Personal info | None | None | + +## Encryption in transit + +Traffic is encrypted when your server is configured with HTTPS. If you configure a plain-HTTP server address, traffic is not encrypted — this is under your control as the server operator. + +## Data deletion + +Uninstalling the app removes all locally stored credentials and settings. Data on your server is governed by your own server configuration. + +## Source code + +The full source is auditable: https://github.com/Dvorinka/cloudreve + +## Contact + +Open an issue at https://github.com/Dvorinka/cloudreve/issues diff --git a/android/.gitignore b/android/.gitignore index 3550d76f..2cb5e96e 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -4,3 +4,4 @@ build/ local.properties *.iml .cxx/ +.kotlin/ diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index d39ef5c6..e0d60583 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -10,11 +10,11 @@ android { compileSdk = 35 defaultConfig { - applicationId = "org.cloudreve.android" + applicationId = "com.tdvorak.cloudreve" minSdk = 28 targetSdk = 35 versionCode = 1 - versionName = "0.1.0" + versionName = "1.0.0" } signingConfigs { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1166f5ca..5a4dba39 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,10 @@ + + @@ -16,6 +20,7 @@ 0) session.chunkSize else localFile.length().coerceAtLeast(1) + val totalChunks = ((localFile.length() + chunkSize - 1) / chunkSize).toInt().coerceAtLeast(1) + val svc = api.service() var sent = 0L localFile.inputStream().buffered().use { input -> val buffer = ByteArray(chunkSize.coerceAtMost(64L * 1024 * 1024).toInt()) - var index = 0 - while (true) { + for (index in 0 until totalChunks) { val read = input.readNBytes(buffer, 0, buffer.size) - if (read <= 0 || index >= urls.size) break - val target = resolveUrl(urls[index]) + if (index == 0 && read == 0 && localFile.length() > 0) break + val target = if (conventional) { + session_serverBase() + "/api/v4/file/upload/${session.sessionId}/$index" + } else { + if (index >= urls!!.size) break + resolveUrl(urls[index]) + } val body = buffer.copyOf(read) .toRequestBody(mime.toMediaTypeOrNull()) val resp = if (target.contains("/file/upload/")) { @@ -138,8 +144,8 @@ class FileRepository(private val api: ApiClient) { } resp.body()?.close() sent += read - index++ onProgress(sent, localFile.length()) + if (read <= 0) break } } diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml deleted file mode 100644 index d02506b4..00000000 --- a/android/app/src/main/res/drawable/ic_launcher_foreground.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index a8a8fa55..be438580 100644 --- a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..be438580 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 00000000..8a30de1c Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..7ff5c93f Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 00000000..8a30de1c Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..33cb9ec3 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..a267f5c3 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..33cb9ec3 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..718af284 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..6851619c Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 00000000..718af284 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 00000000..b664528b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..35b34bd4 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..b664528b Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 00000000..81b80356 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 00000000..4703ab90 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 00000000..81b80356 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index d0f957c0..f42ada65 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,4 +1,4 @@ - #4A6FA5 + #FFFFFF diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 709b645e..dd865ed0 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - Cloudreve + Cloudreve Mobile diff --git a/android/metadata/assets/feature-graphic.png b/android/metadata/assets/feature-graphic.png new file mode 100644 index 00000000..f2c7fd76 Binary files /dev/null and b/android/metadata/assets/feature-graphic.png differ diff --git a/android/metadata/assets/icon-512.png b/android/metadata/assets/icon-512.png new file mode 100644 index 00000000..19fc3b98 Binary files /dev/null and b/android/metadata/assets/icon-512.png differ diff --git a/android/metadata/en-GB/full_description.txt b/android/metadata/en-GB/full_description.txt new file mode 100644 index 00000000..203ec520 --- /dev/null +++ b/android/metadata/en-GB/full_description.txt @@ -0,0 +1,21 @@ +Cloudreve Mobile is the open-source Android client for Cloudreve — the self-hosted cloud storage platform. Connect to your own Cloudreve server and take your files anywhere. + +YOUR FILES, YOUR SERVER +No third-party cloud. Your data lives on infrastructure you control — the app talks directly to the server you configure, nothing else. + +FEATURES +• Browse, upload, and download files and folders +• Create and manage share links +• Full-text search across your drive +• Automatic camera backup with Wi-Fi-only option +• Keep files offline with one tap +• Local sync folder — mirror a device folder to your server +• Android Files app integration (DocumentsProvider) +• Share-sheet target — send files from any app +• Quick Settings tile for one-tap camera backup +• Notifications for uploads, downloads, and background tasks + +OPEN SOURCE +Fully open source — audit the code, self-host the server, own your data. + +Requires a self-hosted Cloudreve server (v4+). Source code and documentation: https://github.com/Dvorinka/cloudreve \ No newline at end of file diff --git a/android/metadata/en-GB/short_description.txt b/android/metadata/en-GB/short_description.txt new file mode 100644 index 00000000..748772e9 --- /dev/null +++ b/android/metadata/en-GB/short_description.txt @@ -0,0 +1 @@ +Your self-hosted cloud — sync, share, and manage files on your terms \ No newline at end of file diff --git a/android/metadata/en-GB/title.txt b/android/metadata/en-GB/title.txt new file mode 100644 index 00000000..41ec8e08 --- /dev/null +++ b/android/metadata/en-GB/title.txt @@ -0,0 +1 @@ +Cloudreve Mobile \ No newline at end of file diff --git a/android/metadata/en-US/full_description.txt b/android/metadata/en-US/full_description.txt new file mode 100644 index 00000000..203ec520 --- /dev/null +++ b/android/metadata/en-US/full_description.txt @@ -0,0 +1,21 @@ +Cloudreve Mobile is the open-source Android client for Cloudreve — the self-hosted cloud storage platform. Connect to your own Cloudreve server and take your files anywhere. + +YOUR FILES, YOUR SERVER +No third-party cloud. Your data lives on infrastructure you control — the app talks directly to the server you configure, nothing else. + +FEATURES +• Browse, upload, and download files and folders +• Create and manage share links +• Full-text search across your drive +• Automatic camera backup with Wi-Fi-only option +• Keep files offline with one tap +• Local sync folder — mirror a device folder to your server +• Android Files app integration (DocumentsProvider) +• Share-sheet target — send files from any app +• Quick Settings tile for one-tap camera backup +• Notifications for uploads, downloads, and background tasks + +OPEN SOURCE +Fully open source — audit the code, self-host the server, own your data. + +Requires a self-hosted Cloudreve server (v4+). Source code and documentation: https://github.com/Dvorinka/cloudreve \ No newline at end of file diff --git a/android/metadata/en-US/short_description.txt b/android/metadata/en-US/short_description.txt new file mode 100644 index 00000000..748772e9 --- /dev/null +++ b/android/metadata/en-US/short_description.txt @@ -0,0 +1 @@ +Your self-hosted cloud — sync, share, and manage files on your terms \ No newline at end of file diff --git a/android/metadata/en-US/title.txt b/android/metadata/en-US/title.txt new file mode 100644 index 00000000..41ec8e08 --- /dev/null +++ b/android/metadata/en-US/title.txt @@ -0,0 +1 @@ +Cloudreve Mobile \ No newline at end of file