Skip to content

Instantly share code, notes, and snippets.

@0n3m0r3
Created September 8, 2026 15:15
Show Gist options
  • Select an option

  • Save 0n3m0r3/3a1ccc9e371186412c687b6ec46cc999 to your computer and use it in GitHub Desktop.

Select an option

Save 0n3m0r3/3a1ccc9e371186412c687b6ec46cc999 to your computer and use it in GitHub Desktop.
Nextcloud Android min file age before auto-upload/delete (#2323)

Nextcloud Android: min file age before auto-upload / delete

One-off for nextcloud/android#2323 (sanchosk / scoopex: would sponsor; gallyamb 2026-09-08: Vivo rewrites the JPEG after bokeh).

UploadDelayPolicy is retry backoff (3.5 s), not file age. Auto-upload still runs as soon as the file appears, then "delete after upload" can yank it from the gallery. This gate skips until now - lastModified >= min_age. min_age 0 is today's behaviour.

No SLA. No warranty. No APK from this Linux box.

Drop-in

Copy FileAgeGate.kt next to AutoUploadWorker.kt. Before createUploadFileOperation:

val minAge = syncedFolder.minAgeMs // new SyncedFolder long, default 0
if (!FileAgeGate.isOldEnough(System.currentTimeMillis(), File(localPath).lastModified(), minAge)) {
    continue // leave it for a later scan
}

Same check in DeleteUploadedFileOperation so a 30-day setting keeps the photo on-device after a successful upload.

Settings strings the gate already parses: 0, 90s, 5m, 1h, 30d.

python3 test_file_age_gate.py → all tests passed. Fresh 1-minute photo with 5m is skipped; Vivo rewrite at t=now is skipped until 2m; 30d matches the storage case.

Payment

You offered to sponsor a delay. One-off 30 USDC on Base if this min-age gate is the slice: 0x89324980acaf83Bc065f1cE7A613BF43f9E5692d. PayPal/bank: say so on this GitHub issue (no secrets). Nothing else owed. No Play Store APK, no UI polish, no 30-day calendar picker unless newly paid.

#!/usr/bin/env python3
"""Min file age before Nextcloud Android auto-upload / local delete.
One-off for https://github.com/nextcloud/android/issues/2323
(sanchosk / scoopex: would sponsor; gallyamb 2026-09-08: Vivo rewrites
the JPEG after bokeh, so an immediate upload stores the unprocessed file).
UploadDelayPolicy in the app is retry backoff (3.5s), not file age.
Gate: skip until (now - lastModified) >= min_age. min_age 0 = today.
No SLA. No warranty. No Android APK from here.
"""
from __future__ import annotations
import re
_UNITS = {
"s": 1000,
"m": 60 * 1000,
"h": 60 * 60 * 1000,
"d": 24 * 60 * 60 * 1000,
}
def parse_min_age(text):
"""'0', '90s', '5m', '1h', '30d' -> milliseconds. Empty/None -> 0."""
if text is None:
return 0
s = str(text).strip().lower()
if s == "" or s == "0":
return 0
m = re.fullmatch(r"(\d+)([smhd])", s)
if not m:
raise ValueError("min_age must be 0 or <n>s/m/h/d")
n = int(m.group(1))
return n * _UNITS[m.group(2)]
def is_old_enough(now_ms, last_modified_ms, min_age_ms):
"""True = auto-upload (and optional local delete) may run."""
min_age_ms = int(min_age_ms)
if min_age_ms <= 0:
return True
last_modified_ms = int(last_modified_ms)
if last_modified_ms < 0:
return False
return (int(now_ms) - last_modified_ms) >= min_age_ms
package com.nextcloud.client.jobs.autoUpload
/**
* Min file age before auto-upload / local delete.
* Same rules as file_age_gate.py. Drop into AutoUploadWorker before
* createUploadFileOperation, and into DeleteUploadedFileOperation.
*
* minAgeMs <= 0 keeps today's behaviour.
*/
object FileAgeGate {
@JvmStatic
fun isOldEnough(nowMs: Long, lastModifiedMs: Long, minAgeMs: Long): Boolean {
if (minAgeMs <= 0L) {
return true
}
if (lastModifiedMs < 0L) {
return false
}
return (nowMs - lastModifiedMs) >= minAgeMs
}
}
#!/usr/bin/env python3
from file_age_gate import is_old_enough, parse_min_age
fail = 0
def check(name, ok, detail=""):
global fail
if ok:
print("ok", name)
return
print("FAIL", name, detail)
fail += 1
# current behaviour: delay off
check("zero_always", is_old_enough(1000, 999, 0) is True)
check("neg_age_off", is_old_enough(1000, 1, -5) is True)
# original issue: wait N minutes after create before upload/delete
# now must be larger than 30d so lastModified stays >= 0
now = 3_000_000_000_000
age_5m = parse_min_age("5m")
check("parse_5m", age_5m == 5 * 60 * 1000)
check("too_fresh", is_old_enough(now, now - 60_000, age_5m) is False)
check("old_enough", is_old_enough(now, now - age_5m, age_5m) is True)
check("exact_boundary", is_old_enough(now, now - age_5m, age_5m) is True)
# gallyamb Vivo: post-process rewrites the file, lastModified jumps to now
vivo_raw = now - 30_000
check("vivo_raw_skipped", is_old_enough(now, vivo_raw, parse_min_age("2m")) is False)
vivo_done = now - parse_min_age("2m")
check("vivo_processed", is_old_enough(now, vivo_done, parse_min_age("2m")) is True)
# sanchosk 30 days before local delete
d30 = parse_min_age("30d")
check("parse_30d", d30 == 30 * 24 * 60 * 60 * 1000)
check("not_30d", is_old_enough(now, now - parse_min_age("29d"), d30) is False)
check("is_30d", is_old_enough(now, now - d30, d30) is True)
check("parse_0", parse_min_age("0") == 0)
check("parse_empty", parse_min_age("") == 0)
check("parse_90s", parse_min_age("90s") == 90_000)
check("parse_1h", parse_min_age("1h") == 3_600_000)
try:
parse_min_age("5x")
check("bad_unit", False)
except ValueError:
check("bad_unit", True)
check("unknown_mtime", is_old_enough(now, -1, 1000) is False)
# Kotlin drop-in must keep the same signature names
kt = open("FileAgeGate.kt", encoding="utf-8").read()
check("kt_fun", "fun isOldEnough" in kt)
check("kt_zero", "minAgeMs <= 0L" in kt)
if fail:
print("FAILED", fail)
raise SystemExit(1)
print("all tests passed")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment