main.c was a single 6018-line translation unit. Cut it at the existing section banners into ordered part files (state, draw, config, net, jpeg, api, thumbs, display, ui, video, sync, app) that main.c #includes in dependency order. The parts form one translation unit and share all file-scope state, so there are no headers, no extern decls, and no de-static-ing of the ~100 shared globals coupling the three threads. Only src/main.c is compiled (CMakeLists unchanged). The slices are byte-exact: the preprocessed TU is identical to the old main.c, so the binary is unchanged. Verified by a clean build plus a Vita3K run (login, paging, grid, detail view, back nav, no crash). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
637 lines
20 KiB
C
637 lines
20 KiB
C
/* vitaImmich unity build — part of one translation unit.
|
|
* This file is #included by src/main.c (in order) and shares all
|
|
* file-scope state with the other parts; it is NOT compiled on its own.
|
|
* Only src/main.c is listed in CMakeLists.txt. Editors/clangd may flag
|
|
* undefined symbols here because of that — those are expected.
|
|
*/
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* local camera media: scan, hash, check, upload */
|
|
/* */
|
|
/* The scan (paths + stat only) runs on the main thread at startup; the */
|
|
/* slow work — SHA1 hashing, the bulk dup-check and uploads — runs on a */
|
|
/* second background thread so the UI never blocks. Item state lives in */
|
|
/* the volatile g_local_state[] machine; the sync thread is the only */
|
|
/* writer except for the QUEUED handoff (main thread, guarded). */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* build "YYYY-MM-DDTHH:MM:SS" (sortable) from a SceDateTime. fields are
|
|
* masked to their printed width so the output can never overrun out[] */
|
|
static void datetime_sortable(const SceDateTime *t, char *out, size_t len)
|
|
{
|
|
snprintf(out, len, "%04d-%02d-%02dT%02d:%02d:%02d",
|
|
t->year % 10000, t->month % 100, t->day % 100,
|
|
t->hour % 100, t->minute % 100, t->second % 100);
|
|
}
|
|
|
|
/* build ISO8601 with milliseconds + Z, as Immich's API expects */
|
|
static void datetime_iso(const SceDateTime *t, char *out, size_t len)
|
|
{
|
|
snprintf(out, len, "%04d-%02d-%02dT%02d:%02d:%02d.000Z",
|
|
t->year % 10000, t->month % 100, t->day % 100,
|
|
t->hour % 100, t->minute % 100, t->second % 100);
|
|
}
|
|
|
|
static int ext_is(const char *name, const char *ext)
|
|
{
|
|
size_t n = strlen(name), e = strlen(ext);
|
|
return n > e && !strcasecmp(name + n - e, ext);
|
|
}
|
|
|
|
static int is_media_name(const char *name, int *is_video)
|
|
{
|
|
if (ext_is(name, ".jpg") || ext_is(name, ".jpeg") || ext_is(name, ".png")) {
|
|
*is_video = 0;
|
|
return 1;
|
|
}
|
|
if (ext_is(name, ".mp4")) {
|
|
*is_video = 1;
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* set while re-scanning after a resume from sleep: scan_dir then only appends
|
|
* genuinely-new files into the headroom (no realloc; the worker/sync threads
|
|
* are reading the arrays) */
|
|
static int g_rescan;
|
|
|
|
/* recurse a directory (up to depth levels) collecting camera media */
|
|
static void scan_dir(const char *path, int depth)
|
|
{
|
|
SceUID dfd = sceIoDopen(path);
|
|
if (dfd < 0) {
|
|
/* a safe (sandboxed) self gets an error here; the vpk must be
|
|
* built UNSAFE and unsafe homebrew enabled in HENkaku settings */
|
|
log_line("scan: cannot open %s: 0x%08x", path, dfd);
|
|
return;
|
|
}
|
|
|
|
SceIoDirent ent;
|
|
memset(&ent, 0, sizeof(ent));
|
|
while (sceIoDread(dfd, &ent) > 0) {
|
|
if (ent.d_name[0] == '.')
|
|
continue;
|
|
|
|
/* build "path/name" by hand (bounded, no snprintf %s
|
|
* truncation warning); skip anything too long to store */
|
|
size_t pl = strlen(path), nl = strlen(ent.d_name);
|
|
if (pl + 1 + nl + 1 > sizeof(g_local_path[0]))
|
|
continue;
|
|
char full[sizeof(g_local_path[0])];
|
|
memcpy(full, path, pl);
|
|
full[pl] = '/';
|
|
memcpy(full + pl + 1, ent.d_name, nl + 1);
|
|
|
|
/* some IO drivers report directory-ness only in st_attr */
|
|
if (SCE_S_ISDIR(ent.d_stat.st_mode) ||
|
|
SCE_SO_ISDIR(ent.d_stat.st_attr)) {
|
|
if (depth > 0)
|
|
scan_dir(full, depth - 1);
|
|
continue;
|
|
}
|
|
|
|
int is_video;
|
|
if (!is_media_name(ent.d_name, &is_video))
|
|
continue;
|
|
if (!strcmp(full, VIDEO_TMP_PATH)) /* our own temp file */
|
|
continue;
|
|
if (g_syncmax_mb > 0 &&
|
|
(long long)ent.d_stat.st_size > g_syncmax_mb * 1024LL * 1024LL) {
|
|
log_line("scan: skip %s (%lld MB > syncmaxmb=%ld)", full,
|
|
(long long)ent.d_stat.st_size >> 20, g_syncmax_mb);
|
|
continue;
|
|
}
|
|
|
|
/* rescan (resume from sleep): skip files we already know, and
|
|
* never realloc while the sync/worker threads are reading the
|
|
* arrays — only fill into the headroom reserved at startup */
|
|
if (g_rescan) {
|
|
int known = 0;
|
|
for (int k = 0; k < g_local_count && !known; k++)
|
|
known = !strcmp(g_local_path[k], full);
|
|
if (known)
|
|
continue;
|
|
if (g_local_count >= g_local_cap) {
|
|
log_line("rescan: no headroom at %d files",
|
|
g_local_count);
|
|
break;
|
|
}
|
|
} else if (!grow_locals(g_local_count + 1)) {
|
|
log_line("scan: out of memory at %d local files",
|
|
g_local_count);
|
|
break;
|
|
}
|
|
int j = g_local_count; /* fill, then publish (count++) */
|
|
memcpy(g_local_path[j], full, pl + 1 + nl + 1);
|
|
g_local_is_video[j] = is_video;
|
|
g_local_size[j] = (long long)ent.d_stat.st_size;
|
|
g_local_mtime[j] = ent.d_stat.st_mtime;
|
|
g_local_ctime[j] = ent.d_stat.st_ctime;
|
|
datetime_sortable(&ent.d_stat.st_mtime, g_local_date[j],
|
|
sizeof(g_local_date[j]));
|
|
g_local_sha1[j][0] = '\0';
|
|
g_local_server_id[j][0] = '\0';
|
|
g_local_err[j][0] = '\0';
|
|
g_local_state[j] = SYNC_UNSCANNED;
|
|
__sync_synchronize();
|
|
g_local_count = j + 1;
|
|
}
|
|
sceIoDclose(dfd);
|
|
log_line("scan: %s: %d media so far", path, g_local_count);
|
|
}
|
|
|
|
/* scan the Vita's camera folders (main thread, fast: paths + stat only) */
|
|
static int g_photo0_mounted;
|
|
|
|
static void scan_local_media(void)
|
|
{
|
|
g_local_count = 0;
|
|
if (g_syncdir_count > 0) {
|
|
for (int i = 0; i < g_syncdir_count; i++)
|
|
scan_dir(g_syncdirs[i], 3);
|
|
} else {
|
|
/* ux0:picture is ACL-protected (EPERM) even for unsafe
|
|
* homebrew; the photo0: appdata mount is the sanctioned way
|
|
* in. Camera videos live there too (the Photos app owns
|
|
* them), so all of ux0:video — the movie collection — is
|
|
* left alone. */
|
|
int mres = sceAppMgrAppDataMount(100, "photo0:");
|
|
g_photo0_mounted = (mres == 0);
|
|
log_line("scan: photo0 mount: 0x%08x", mres);
|
|
scan_dir(g_photo0_mounted ? "photo0:" : "ux0:picture", 3);
|
|
scan_dir("ux0:video/CAMERA", 3);
|
|
}
|
|
log_line("local scan: %d files", g_local_count);
|
|
}
|
|
|
|
/* append-only re-scan of the camera folders (resume from sleep). same dirs as
|
|
* scan_local_media, but g_rescan keeps it from touching known entries or
|
|
* reallocating. returns how many new files were appended. */
|
|
static int rescan_local_new(void)
|
|
{
|
|
int before = g_local_count;
|
|
g_rescan = 1;
|
|
if (g_syncdir_count > 0) {
|
|
for (int i = 0; i < g_syncdir_count; i++)
|
|
scan_dir(g_syncdirs[i], 3);
|
|
} else {
|
|
scan_dir(g_photo0_mounted ? "photo0:" : "ux0:picture", 3);
|
|
scan_dir("ux0:video/CAMERA", 3);
|
|
}
|
|
g_rescan = 0;
|
|
int added = g_local_count - before;
|
|
if (added > 0)
|
|
log_line("rescan: %d new local file(s) after resume", added);
|
|
return added;
|
|
}
|
|
|
|
static int sha1_file_hex(const char *path, char out[41])
|
|
{
|
|
FILE *f = fopen(path, "rb");
|
|
if (!f)
|
|
return -1;
|
|
SHA_CTX c;
|
|
SHA1_Init(&c);
|
|
static unsigned char buf[64 * 1024]; /* big, keep off the stack */
|
|
size_t n;
|
|
while ((n = fread(buf, 1, sizeof(buf), f)) > 0)
|
|
SHA1_Update(&c, buf, n);
|
|
int err = ferror(f);
|
|
fclose(f);
|
|
if (err)
|
|
return -1;
|
|
unsigned char d[20];
|
|
SHA1_Final(d, &c);
|
|
for (int i = 0; i < 20; i++)
|
|
snprintf(out + 2 * i, 3, "%02x", d[i]);
|
|
out[40] = '\0';
|
|
return 0;
|
|
}
|
|
|
|
static void sync_push_err(const char *fmt, ...)
|
|
{
|
|
char buf[160];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(buf, sizeof(buf), fmt, ap);
|
|
va_end(ap);
|
|
snprintf(g_sync_errlog[g_sync_errn % 5], 160, "%s", buf);
|
|
g_sync_errn++;
|
|
log_line("sync: %s", buf);
|
|
}
|
|
|
|
/* one curl handle reused by the sync thread (bulk-check + uploads) */
|
|
static CURL *g_sync_curl;
|
|
static struct curl_slist *g_sync_hdrs;
|
|
|
|
static void sync_curl_init(void)
|
|
{
|
|
g_sync_curl = curl_easy_init();
|
|
char keyhdr[300];
|
|
auth_header(keyhdr, sizeof(keyhdr));
|
|
g_sync_hdrs = curl_slist_append(NULL, keyhdr);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_WRITEFUNCTION, write_cb);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_FOLLOWLOCATION, 1L);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_CONNECTTIMEOUT, 10L);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_USERAGENT, "vitaImmich/0.1 (PS Vita)");
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_SSL_VERIFYPEER, 0L);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_SSL_VERIFYHOST, 0L);
|
|
if (g_resolve_list)
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_RESOLVE, g_resolve_list);
|
|
}
|
|
|
|
/* parse a bulk-upload-check response: for each result, look up the local
|
|
* item by its id (we send the local index as the id) and set its state */
|
|
static void parse_bulk_check(const char *js, size_t jslen)
|
|
{
|
|
jsmn_parser parser;
|
|
jsmn_init(&parser);
|
|
int ntok = jsmn_parse(&parser, js, jslen, NULL, 0);
|
|
if (ntok <= 0)
|
|
return;
|
|
jsmntok_t *tok = malloc(sizeof(jsmntok_t) * ntok);
|
|
if (!tok)
|
|
return;
|
|
jsmn_init(&parser);
|
|
ntok = jsmn_parse(&parser, js, jslen, tok, ntok);
|
|
if (ntok <= 0) {
|
|
free(tok);
|
|
return;
|
|
}
|
|
|
|
/* walk objects, collecting id/action/reason/assetId per result */
|
|
int cur_obj = -1, li = -1;
|
|
char action[16], reason[24], assetid[40];
|
|
for (int i = 0; i <= ntok; i++) {
|
|
if (i == ntok || tok[i].type == JSMN_OBJECT) {
|
|
/* flush the previous object's result */
|
|
if (li >= 0 && li < g_local_count) {
|
|
if (!strcmp(action, "reject") &&
|
|
!strcmp(reason, "duplicate")) {
|
|
snprintf(g_local_server_id[li], 40,
|
|
"%s", assetid);
|
|
g_local_state[li] = SYNC_BACKED_UP;
|
|
} else {
|
|
/* accept, or a non-duplicate reject we
|
|
* can still try to upload */
|
|
g_local_state[li] = SYNC_LOCAL_ONLY;
|
|
}
|
|
g_need_rebuild = 1;
|
|
}
|
|
if (i == ntok)
|
|
break;
|
|
cur_obj = i;
|
|
li = -1;
|
|
action[0] = reason[0] = assetid[0] = '\0';
|
|
continue;
|
|
}
|
|
if (tok[i].type != JSMN_STRING || tok[i].size != 1 ||
|
|
tok[i].parent != cur_obj || i + 1 >= ntok)
|
|
continue;
|
|
int klen = tok[i].end - tok[i].start;
|
|
const char *k = js + tok[i].start;
|
|
jsmntok_t *v = &tok[i + 1];
|
|
int vlen = v->end - v->start;
|
|
const char *vs = js + v->start;
|
|
if (klen == 2 && !strncmp(k, "id", 2))
|
|
li = atoi(vs); /* we sent the local index */
|
|
else if (klen == 6 && !strncmp(k, "action", 6))
|
|
snprintf(action, sizeof(action), "%.*s", vlen, vs);
|
|
else if (klen == 6 && !strncmp(k, "reason", 6))
|
|
snprintf(reason, sizeof(reason), "%.*s", vlen, vs);
|
|
else if (klen == 7 && !strncmp(k, "assetId", 7))
|
|
snprintf(assetid, sizeof(assetid), "%.*s", vlen, vs);
|
|
}
|
|
free(tok);
|
|
}
|
|
|
|
/* bulk-upload-check a batch of CHECKING items (indices in idxs[0..n)) */
|
|
static void bulk_check_batch(const int *idxs, int n)
|
|
{
|
|
/* {"assets":[{"id":"<localidx>","checksum":"<hex>"},...]} */
|
|
size_t cap = 64 + n * 80;
|
|
char *body = malloc(cap);
|
|
if (!body)
|
|
return;
|
|
int off = snprintf(body, cap, "{\"assets\":[");
|
|
for (int i = 0; i < n; i++)
|
|
off += snprintf(body + off, cap - off,
|
|
"%s{\"id\":\"%d\",\"checksum\":\"%s\"}",
|
|
i ? "," : "", idxs[i], g_local_sha1[idxs[i]]);
|
|
off += snprintf(body + off, cap - off, "]}");
|
|
|
|
char url[600];
|
|
snprintf(url, sizeof(url), "%s/api/assets/bulk-upload-check", g_server);
|
|
|
|
struct curl_slist *hdrs = curl_slist_append(NULL, "Content-Type: application/json");
|
|
char keyhdr[300];
|
|
auth_header(keyhdr, sizeof(keyhdr));
|
|
hdrs = curl_slist_append(hdrs, keyhdr);
|
|
|
|
membuf buf = { NULL, 0 };
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_HTTPHEADER, hdrs);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_POST, 1L);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_POSTFIELDS, body);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_WRITEDATA, &buf);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_TIMEOUT, 60L);
|
|
net_lock(); /* TLS is single-threaded, see g_net_mutex */
|
|
CURLcode res = curl_easy_perform(g_sync_curl);
|
|
net_unlock();
|
|
long code = 0;
|
|
if (res == CURLE_OK)
|
|
curl_easy_getinfo(g_sync_curl, CURLINFO_RESPONSE_CODE, &code);
|
|
|
|
if (res == CURLE_OK && code >= 200 && code < 300 && buf.data) {
|
|
parse_bulk_check(buf.data, buf.size);
|
|
} else {
|
|
/* leave them LOCAL_ONLY so the user can still try to upload */
|
|
for (int i = 0; i < n; i++)
|
|
g_local_state[idxs[i]] = SYNC_LOCAL_ONLY;
|
|
sync_push_err("dup-check failed (curl %d, HTTP %ld)", res, code);
|
|
}
|
|
g_need_rebuild = 1;
|
|
|
|
/* reset the handle for reuse (clear POSTFIELDS / restore defaults) */
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_HTTPHEADER, NULL);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_POSTFIELDS, NULL);
|
|
curl_slist_free_all(hdrs);
|
|
free(buf.data);
|
|
free(body);
|
|
}
|
|
|
|
static int upload_xfer_cb(void *ud, curl_off_t dl, curl_off_t dln,
|
|
curl_off_t ult, curl_off_t uln)
|
|
{
|
|
g_ul_total = ult;
|
|
g_ul_now = uln;
|
|
return 0;
|
|
}
|
|
|
|
/* upload one local item; returns 0 on success, -1 on failure (err set) */
|
|
static int upload_item(int j, char *err, size_t errlen)
|
|
{
|
|
const char *path = g_local_path[j];
|
|
const char *name = strrchr(path, '/');
|
|
name = name ? name + 1 : path;
|
|
|
|
char created[40], modified[40], devid[300];
|
|
datetime_iso(&g_local_ctime[j], created, sizeof(created));
|
|
datetime_iso(&g_local_mtime[j], modified, sizeof(modified));
|
|
snprintf(devid, sizeof(devid), "%s-%lld", name, g_local_size[j]);
|
|
|
|
char url[600];
|
|
snprintf(url, sizeof(url), "%s/api/assets", g_server);
|
|
|
|
char keyhdr[300], sumhdr[80];
|
|
auth_header(keyhdr, sizeof(keyhdr));
|
|
snprintf(sumhdr, sizeof(sumhdr), "x-immich-checksum: %s", g_local_sha1[j]);
|
|
struct curl_slist *hdrs = curl_slist_append(NULL, keyhdr);
|
|
hdrs = curl_slist_append(hdrs, sumhdr);
|
|
hdrs = curl_slist_append(hdrs, "Accept: application/json");
|
|
|
|
curl_mime *mime = curl_mime_init(g_sync_curl);
|
|
curl_mimepart *part;
|
|
part = curl_mime_addpart(mime);
|
|
curl_mime_name(part, "assetData");
|
|
curl_mime_filedata(part, path); /* streams from disk */
|
|
curl_mime_filename(part, name);
|
|
part = curl_mime_addpart(mime);
|
|
curl_mime_name(part, "deviceAssetId");
|
|
curl_mime_data(part, devid, CURL_ZERO_TERMINATED);
|
|
part = curl_mime_addpart(mime);
|
|
curl_mime_name(part, "deviceId");
|
|
curl_mime_data(part, "PS Vita", CURL_ZERO_TERMINATED);
|
|
part = curl_mime_addpart(mime);
|
|
curl_mime_name(part, "fileCreatedAt");
|
|
curl_mime_data(part, created, CURL_ZERO_TERMINATED);
|
|
part = curl_mime_addpart(mime);
|
|
curl_mime_name(part, "fileModifiedAt");
|
|
curl_mime_data(part, modified, CURL_ZERO_TERMINATED);
|
|
part = curl_mime_addpart(mime);
|
|
curl_mime_name(part, "filename");
|
|
curl_mime_data(part, name, CURL_ZERO_TERMINATED);
|
|
|
|
membuf buf = { NULL, 0 };
|
|
g_ul_now = g_ul_total = 0;
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_URL, url);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_HTTPHEADER, hdrs);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_MIMEPOST, mime);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_WRITEDATA, &buf);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_TIMEOUT, 0L); /* big files */
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_LOW_SPEED_LIMIT, 512L);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_LOW_SPEED_TIME, 60L);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_NOPROGRESS, 0L);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_XFERINFOFUNCTION, upload_xfer_cb);
|
|
|
|
net_lock(); /* TLS is single-threaded, see g_net_mutex */
|
|
CURLcode res = curl_easy_perform(g_sync_curl);
|
|
net_unlock();
|
|
long code = 0;
|
|
if (res == CURLE_OK)
|
|
curl_easy_getinfo(g_sync_curl, CURLINFO_RESPONSE_CODE, &code);
|
|
|
|
int rc = -1;
|
|
if (res != CURLE_OK) {
|
|
snprintf(err, errlen, "curl %d: %s", res, curl_easy_strerror(res));
|
|
} else if (code != 200 && code != 201) {
|
|
snprintf(err, errlen, "HTTP %ld: %.80s", code,
|
|
buf.data ? buf.data : "");
|
|
} else {
|
|
/* parse {"id":"...","status":"created|duplicate"} */
|
|
const char *p = buf.data ? strstr(buf.data, "\"id\"") : NULL;
|
|
if (p) {
|
|
p = strchr(p + 4, '"');
|
|
if (p) {
|
|
p++;
|
|
int k = 0;
|
|
while (p[k] && p[k] != '"' && k < 39) {
|
|
g_local_server_id[j][k] = p[k];
|
|
k++;
|
|
}
|
|
g_local_server_id[j][k] = '\0';
|
|
}
|
|
}
|
|
rc = 0;
|
|
}
|
|
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_MIMEPOST, NULL);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_NOPROGRESS, 1L);
|
|
curl_easy_setopt(g_sync_curl, CURLOPT_HTTPHEADER, NULL);
|
|
curl_mime_free(mime);
|
|
curl_slist_free_all(hdrs);
|
|
free(buf.data);
|
|
return rc;
|
|
}
|
|
|
|
/* the sync worker: hash everything, dup-check in batches, then drain the
|
|
* upload queue (items the user moved to QUEUED from the sync screen) */
|
|
static int sync_thread(SceSize args, void *argp)
|
|
{
|
|
sync_curl_init();
|
|
|
|
for (;;) {
|
|
/* idle while the main thread resets for an account switch */
|
|
if (g_pause_bg) {
|
|
g_sync_idle = 1;
|
|
sceKernelDelayThread(8 * 1000);
|
|
continue;
|
|
}
|
|
g_sync_idle = 0;
|
|
|
|
/* 1. hash + dup-check a batch of freshly scanned files */
|
|
int batch[100], nb = 0;
|
|
for (int j = 0; j < g_local_count && nb < 100; j++) {
|
|
if (g_local_state[j] != SYNC_UNSCANNED)
|
|
continue;
|
|
const char *name = strrchr(g_local_path[j], '/');
|
|
name = name ? name + 1 : g_local_path[j];
|
|
g_sync_phase = 1;
|
|
snprintf(g_sync_activity, sizeof(g_sync_activity),
|
|
"hashing %.120s", name);
|
|
g_local_state[j] = SYNC_HASHING;
|
|
if (sha1_file_hex(g_local_path[j], g_local_sha1[j]) != 0) {
|
|
snprintf(g_local_err[j], sizeof(g_local_err[j]),
|
|
"hash failed");
|
|
g_local_state[j] = SYNC_FAILED;
|
|
sync_push_err("hash failed: %s", name);
|
|
continue;
|
|
}
|
|
g_local_state[j] = SYNC_CHECKING;
|
|
batch[nb++] = j;
|
|
}
|
|
if (nb > 0) {
|
|
g_sync_phase = 2;
|
|
snprintf(g_sync_activity, sizeof(g_sync_activity),
|
|
"checking %d files on server", nb);
|
|
bulk_check_batch(batch, nb);
|
|
continue; /* loop back for the next batch */
|
|
}
|
|
|
|
/* 2. drain the upload queue */
|
|
int up = -1;
|
|
for (int j = 0; j < g_local_count; j++)
|
|
if (g_local_state[j] == SYNC_QUEUED) {
|
|
up = j;
|
|
break;
|
|
}
|
|
if (up >= 0) {
|
|
const char *name = strrchr(g_local_path[up], '/');
|
|
name = name ? name + 1 : g_local_path[up];
|
|
g_local_state[up] = SYNC_UPLOADING;
|
|
g_sync_phase = 3;
|
|
snprintf(g_sync_activity, sizeof(g_sync_activity),
|
|
"uploading %.118s", name);
|
|
char err[96];
|
|
if (upload_item(up, err, sizeof(err)) == 0) {
|
|
g_local_state[up] = SYNC_BACKED_UP;
|
|
} else {
|
|
snprintf(g_local_err[up], sizeof(g_local_err[up]),
|
|
"%s", err);
|
|
g_local_state[up] = SYNC_FAILED;
|
|
sync_push_err("upload %s: %s", name, err);
|
|
}
|
|
g_need_rebuild = 1;
|
|
continue;
|
|
}
|
|
|
|
/* nothing to do */
|
|
g_sync_phase = 0;
|
|
g_sync_activity[0] = '\0';
|
|
sceKernelDelayThread(50 * 1000);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* main thread: queue every LOCAL_ONLY item for upload (the QUEUED handoff,
|
|
* the one transition the main thread is allowed to make) */
|
|
static int queue_all_uploads(void)
|
|
{
|
|
int n = 0;
|
|
for (int j = 0; j < g_local_count; j++)
|
|
if (g_local_state[j] == SYNC_LOCAL_ONLY) {
|
|
g_local_state[j] = SYNC_QUEUED;
|
|
n++;
|
|
}
|
|
__sync_synchronize();
|
|
return n;
|
|
}
|
|
|
|
static void load_autobackup(void)
|
|
{
|
|
FILE *f = fopen(AUTOBK_PATH, "r");
|
|
if (!f)
|
|
return;
|
|
int v = 0;
|
|
if (fscanf(f, "%d", &v) == 1)
|
|
g_autobackup = (v != 0);
|
|
fclose(f);
|
|
}
|
|
|
|
static void save_autobackup(void)
|
|
{
|
|
FILE *f = fopen(AUTOBK_PATH, "w");
|
|
if (f) {
|
|
fputc(g_autobackup ? '1' : '0', f);
|
|
fclose(f);
|
|
}
|
|
}
|
|
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* grid status badges + sync overview */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* small colour dot in the top-left corner of a grid cell; a backed-up
|
|
* item also gets a tiny white check, a local-only item an up-arrow */
|
|
static void draw_status_badge(float bx, float by, float bw, float bh,
|
|
int d, unsigned int frame)
|
|
{
|
|
struct disp_item *it = &g_disp[d];
|
|
int kind; /* 0 none, 1 cloud, 2 local, 3 busy, 4 backed, 5 failed */
|
|
|
|
if (it->src == SRC_SERVER) {
|
|
kind = g_asset_local_backed[it->idx] ? 4 : 1;
|
|
} else {
|
|
switch (g_local_state[it->idx]) {
|
|
case SYNC_LOCAL_ONLY: kind = 2; break;
|
|
case SYNC_QUEUED:
|
|
case SYNC_UPLOADING: kind = 3; break;
|
|
case SYNC_BACKED_UP: kind = 4; break;
|
|
case SYNC_FAILED: kind = 5; break;
|
|
default: kind = 0; break; /* hashing/checking */
|
|
}
|
|
}
|
|
if (kind == 0)
|
|
return;
|
|
|
|
/* Immich cloud status icons (the bundled SVGs): cloud = on server only,
|
|
* cloud-off = on device only, cloud-done = on both. White, bottom-right
|
|
* of the cell, no background; a 1px dark shadow keeps it legible. */
|
|
vita2d_texture *ic = (kind == 4) ? g_ic_both :
|
|
(kind == 1) ? g_ic_server : g_ic_device;
|
|
if (!ic)
|
|
return;
|
|
const float isz = 26.0f;
|
|
float ix = bx + bw - isz - 4; /* bottom-right corner */
|
|
float iy = by + bh - isz - 4;
|
|
float s = isz / vita2d_texture_get_width(ic);
|
|
uint32_t fg = (kind == 5) ? RGBA8(235, 90, 90, 255)
|
|
: RGBA8(255, 255, 255, 255);
|
|
vita2d_draw_texture_tint_scale(ic, ix + 1, iy + 1, s, s,
|
|
RGBA8(0, 0, 0, 140)); /* shadow */
|
|
vita2d_draw_texture_tint_scale(ic, ix, iy, s, s, fg);
|
|
}
|
|
|
|
/* count local items in a given state */
|
|
static int count_state(int st)
|
|
{
|
|
int n = 0;
|
|
for (int j = 0; j < g_local_count; j++)
|
|
if (g_local_state[j] == st)
|
|
n++;
|
|
return n;
|
|
}
|
|
|