@gpc-cli/core 0.10.5 → 0.10.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +14 -2
- package/dist/index.js +119 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -886,7 +886,9 @@ declare function listReportObjects(auth: ReportsAuth, bucket: string, options: {
|
|
|
886
886
|
* content-encoding:gzip objects on this path in Node's fetch, but callers must still run the
|
|
887
887
|
* bytes through the decode layer, which re-checks the gzip magic as a backstop.
|
|
888
888
|
*/
|
|
889
|
-
declare function downloadReportObject(auth: ReportsAuth, bucket: string, objectName: string
|
|
889
|
+
declare function downloadReportObject(auth: ReportsAuth, bucket: string, objectName: string, options?: {
|
|
890
|
+
maxBytes?: number;
|
|
891
|
+
}): Promise<Buffer>;
|
|
890
892
|
|
|
891
893
|
declare function isGzip(buf: Uint8Array): boolean;
|
|
892
894
|
declare function isZip(buf: Uint8Array): boolean;
|
|
@@ -907,13 +909,23 @@ interface ZipCsvEntry {
|
|
|
907
909
|
name: string;
|
|
908
910
|
text: string;
|
|
909
911
|
}
|
|
912
|
+
interface ExtractCsvEntriesOptions {
|
|
913
|
+
/** Ceiling on the inflated size of a single CSV entry. Defaults to MAX_INFLATED_BYTES. */
|
|
914
|
+
maxEntryBytes?: number;
|
|
915
|
+
/** Ceiling on the decoded text of all entries combined. Defaults to MAX_INFLATED_BYTES. */
|
|
916
|
+
maxTotalBytes?: number;
|
|
917
|
+
}
|
|
910
918
|
/**
|
|
911
919
|
* Extract the CSV entries from a financial report ZIP archive, returned as decoded text.
|
|
912
920
|
* Uses yauzl's `fromBuffer` (no file descriptor) so it behaves identically under the
|
|
913
921
|
* Bun-compiled standalone binary, and is bounded by a timeout so a corrupt archive can
|
|
914
922
|
* never hang the process (same hardening as the preflight AAB reader).
|
|
923
|
+
*
|
|
924
|
+
* Each entry is also bounded by `maxEntryBytes`: a ZIP entry's declared sizes are
|
|
925
|
+
* attacker-controlled, so a small archive can otherwise inflate to gigabytes of buffers
|
|
926
|
+
* (a 408 KB archive reached 1.35 GB RSS before this cap).
|
|
915
927
|
*/
|
|
916
|
-
declare function extractCsvEntriesFromZip(buffer: Buffer): Promise<ZipCsvEntry[]>;
|
|
928
|
+
declare function extractCsvEntriesFromZip(buffer: Buffer, options?: ExtractCsvEntriesOptions): Promise<ZipCsvEntry[]>;
|
|
917
929
|
|
|
918
930
|
declare function isFinancialReportType(type: string): boolean;
|
|
919
931
|
declare function isStatsReportType(type: string): boolean;
|
package/dist/index.js
CHANGED
|
@@ -2315,28 +2315,18 @@ function reviewsToCsv(reviews) {
|
|
|
2315
2315
|
const uc = r.comments?.[0]?.userComment;
|
|
2316
2316
|
const fields = [
|
|
2317
2317
|
r.reviewId,
|
|
2318
|
-
|
|
2318
|
+
escapeCsvField(r.authorName),
|
|
2319
2319
|
uc?.starRating ?? "",
|
|
2320
|
-
|
|
2320
|
+
escapeCsvField(uc?.text ?? ""),
|
|
2321
2321
|
uc?.reviewerLanguage ?? "",
|
|
2322
2322
|
uc ? new Date(Number(uc.lastModified.seconds) * 1e3).toISOString() : "",
|
|
2323
|
-
|
|
2324
|
-
|
|
2323
|
+
escapeCsvField(uc?.device ?? ""),
|
|
2324
|
+
escapeCsvField(uc?.appVersionName ?? "")
|
|
2325
2325
|
];
|
|
2326
2326
|
return fields.join(",");
|
|
2327
2327
|
});
|
|
2328
2328
|
return [header, ...rows].join("\n");
|
|
2329
2329
|
}
|
|
2330
|
-
function csvEscape(value) {
|
|
2331
|
-
let safe = value;
|
|
2332
|
-
if (/^[=+\-@\t\r]/.test(safe)) {
|
|
2333
|
-
safe = `'${safe}`;
|
|
2334
|
-
}
|
|
2335
|
-
if (safe.includes(",") || safe.includes('"') || safe.includes("\n")) {
|
|
2336
|
-
return `"${safe.replace(/"/g, '""')}"`;
|
|
2337
|
-
}
|
|
2338
|
-
return safe;
|
|
2339
|
-
}
|
|
2340
2330
|
async function analyzeReviews2(client, packageName, options) {
|
|
2341
2331
|
const { reviews } = await listReviews(client, packageName, options);
|
|
2342
2332
|
const items = reviews.map((r) => {
|
|
@@ -4151,6 +4141,7 @@ function reportObjectNotFoundError(opts) {
|
|
|
4151
4141
|
var GCS_API_BASE = "https://storage.googleapis.com/storage/v1/b";
|
|
4152
4142
|
var LIST_TIMEOUT_MS = 6e4;
|
|
4153
4143
|
var DOWNLOAD_TIMEOUT_MS = 12e4;
|
|
4144
|
+
var MAX_DOWNLOAD_BYTES = 512 * 1024 * 1024;
|
|
4154
4145
|
function networkError(bucket, cause) {
|
|
4155
4146
|
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
4156
4147
|
return new GpcError(
|
|
@@ -4160,6 +4151,14 @@ function networkError(bucket, cause) {
|
|
|
4160
4151
|
"Check your internet connection and retry. Google Cloud Storage may also be briefly unavailable."
|
|
4161
4152
|
);
|
|
4162
4153
|
}
|
|
4154
|
+
function tooLargeError(objectName, maxBytes) {
|
|
4155
|
+
return new GpcError(
|
|
4156
|
+
`Report object "${objectName}" is larger than the ${maxBytes}-byte download limit.`,
|
|
4157
|
+
"REPORT_DOWNLOAD_FAILED",
|
|
4158
|
+
4,
|
|
4159
|
+
"Play bulk reports are far smaller than this; confirm the bucket and object name, then download the file from Play Console -> Download reports if you really need it."
|
|
4160
|
+
);
|
|
4161
|
+
}
|
|
4163
4162
|
function maxRetries() {
|
|
4164
4163
|
const parsed = Number(process.env["GPC_MAX_RETRIES"]);
|
|
4165
4164
|
return Number.isInteger(parsed) && parsed >= 0 && parsed <= 10 ? parsed : 3;
|
|
@@ -4226,7 +4225,8 @@ async function listReportObjects(auth, bucket, options) {
|
|
|
4226
4225
|
}));
|
|
4227
4226
|
return { objects, nextPageToken: body.nextPageToken };
|
|
4228
4227
|
}
|
|
4229
|
-
async function downloadReportObject(auth, bucket, objectName) {
|
|
4228
|
+
async function downloadReportObject(auth, bucket, objectName, options = {}) {
|
|
4229
|
+
const maxBytes = options.maxBytes ?? MAX_DOWNLOAD_BYTES;
|
|
4230
4230
|
const url = `${GCS_API_BASE}/${encodeURIComponent(bucket)}/o/${encodeURIComponent(objectName)}?alt=media`;
|
|
4231
4231
|
const res = await gcsFetch(auth, bucket, url, DOWNLOAD_TIMEOUT_MS);
|
|
4232
4232
|
if (res.status === 404) {
|
|
@@ -4245,11 +4245,25 @@ async function downloadReportObject(auth, bucket, objectName) {
|
|
|
4245
4245
|
"Retry in a moment. If the error persists, list the reports to confirm the object still exists."
|
|
4246
4246
|
);
|
|
4247
4247
|
}
|
|
4248
|
+
const declared = Number(res.headers.get("content-length"));
|
|
4249
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
4250
|
+
throw tooLargeError(objectName, maxBytes);
|
|
4251
|
+
}
|
|
4252
|
+
const body = res.body;
|
|
4253
|
+
if (!body) return Buffer.alloc(0);
|
|
4254
|
+
const chunks = [];
|
|
4255
|
+
let read = 0;
|
|
4248
4256
|
try {
|
|
4249
|
-
|
|
4257
|
+
for await (const chunk of body) {
|
|
4258
|
+
read += chunk.length;
|
|
4259
|
+
if (read > maxBytes) throw tooLargeError(objectName, maxBytes);
|
|
4260
|
+
chunks.push(Buffer.from(chunk));
|
|
4261
|
+
}
|
|
4250
4262
|
} catch (err) {
|
|
4263
|
+
if (err instanceof GpcError) throw err;
|
|
4251
4264
|
throw networkError(bucket, err);
|
|
4252
4265
|
}
|
|
4266
|
+
return Buffer.concat(chunks);
|
|
4253
4267
|
}
|
|
4254
4268
|
|
|
4255
4269
|
// src/reports/decode.ts
|
|
@@ -4285,13 +4299,15 @@ function decodeStatsCsv(buf) {
|
|
|
4285
4299
|
return decodeCsvText(gunzipIfNeeded(buf));
|
|
4286
4300
|
}
|
|
4287
4301
|
var ZIP_READ_TIMEOUT_MS = 3e4;
|
|
4288
|
-
|
|
4302
|
+
var MAX_CSV_ENTRIES = 64;
|
|
4303
|
+
function extractCsvEntriesFromZip(buffer, options = {}) {
|
|
4304
|
+
const maxEntryBytes = options.maxEntryBytes ?? MAX_INFLATED_BYTES;
|
|
4305
|
+
const maxTotalBytes = options.maxTotalBytes ?? MAX_INFLATED_BYTES;
|
|
4289
4306
|
return new Promise((resolve2, reject) => {
|
|
4290
4307
|
let settled = false;
|
|
4291
4308
|
const timer = setTimeout(() => {
|
|
4292
4309
|
settleReject(new Error("Timed out reading report archive"));
|
|
4293
4310
|
}, ZIP_READ_TIMEOUT_MS);
|
|
4294
|
-
timer.unref?.();
|
|
4295
4311
|
function settleResolve(v) {
|
|
4296
4312
|
if (settled) return;
|
|
4297
4313
|
settled = true;
|
|
@@ -4314,6 +4330,8 @@ function extractCsvEntriesFromZip(buffer) {
|
|
|
4314
4330
|
const entries = [];
|
|
4315
4331
|
let pending = 0;
|
|
4316
4332
|
let entriesDone = false;
|
|
4333
|
+
let csvEntries = 0;
|
|
4334
|
+
let totalBytes = 0;
|
|
4317
4335
|
function fail(e) {
|
|
4318
4336
|
try {
|
|
4319
4337
|
zipfile.close();
|
|
@@ -4329,6 +4347,10 @@ function extractCsvEntriesFromZip(buffer) {
|
|
|
4329
4347
|
zipfile.readEntry();
|
|
4330
4348
|
return;
|
|
4331
4349
|
}
|
|
4350
|
+
if (++csvEntries > MAX_CSV_ENTRIES) {
|
|
4351
|
+
fail(new Error(`Report archive holds more than ${MAX_CSV_ENTRIES} CSV entries`));
|
|
4352
|
+
return;
|
|
4353
|
+
}
|
|
4332
4354
|
pending++;
|
|
4333
4355
|
zipfile.openReadStream(entry, (streamErr, stream) => {
|
|
4334
4356
|
if (streamErr || !stream) {
|
|
@@ -4336,10 +4358,35 @@ function extractCsvEntriesFromZip(buffer) {
|
|
|
4336
4358
|
return;
|
|
4337
4359
|
}
|
|
4338
4360
|
const chunks = [];
|
|
4339
|
-
|
|
4361
|
+
let read = 0;
|
|
4362
|
+
stream.on("data", (c) => {
|
|
4363
|
+
read += c.length;
|
|
4364
|
+
if (read > maxEntryBytes) {
|
|
4365
|
+
stream.destroy();
|
|
4366
|
+
fail(
|
|
4367
|
+
new Error(
|
|
4368
|
+
`Entry "${entry.fileName}" exceeds the ${maxEntryBytes}-byte limit for a report CSV`
|
|
4369
|
+
)
|
|
4370
|
+
);
|
|
4371
|
+
return;
|
|
4372
|
+
}
|
|
4373
|
+
chunks.push(c);
|
|
4374
|
+
});
|
|
4340
4375
|
stream.on("error", fail);
|
|
4341
4376
|
stream.on("end", () => {
|
|
4342
|
-
|
|
4377
|
+
let text;
|
|
4378
|
+
try {
|
|
4379
|
+
text = decodeStatsCsv(Buffer.concat(chunks));
|
|
4380
|
+
} catch (e) {
|
|
4381
|
+
fail(e instanceof Error ? e : new Error(String(e)));
|
|
4382
|
+
return;
|
|
4383
|
+
}
|
|
4384
|
+
totalBytes += text.length;
|
|
4385
|
+
if (totalBytes > maxTotalBytes) {
|
|
4386
|
+
fail(new Error(`Report archive exceeds the ${maxTotalBytes}-byte total limit`));
|
|
4387
|
+
return;
|
|
4388
|
+
}
|
|
4389
|
+
entries.push({ name: entry.fileName, text });
|
|
4343
4390
|
pending--;
|
|
4344
4391
|
zipfile.readEntry();
|
|
4345
4392
|
maybeResolve();
|
|
@@ -5458,11 +5505,11 @@ async function getAchievementConfig(client, achievementId) {
|
|
|
5458
5505
|
}
|
|
5459
5506
|
async function createAchievementConfig(client, applicationId, data) {
|
|
5460
5507
|
validateApplicationId(applicationId);
|
|
5461
|
-
return client.achievements.insert(applicationId, data);
|
|
5508
|
+
return client.achievements.insert(applicationId, withoutRedactedToken(data));
|
|
5462
5509
|
}
|
|
5463
5510
|
async function updateAchievementConfig(client, achievementId, data) {
|
|
5464
5511
|
validateResourceId(achievementId, "achievement");
|
|
5465
|
-
return client.achievements.update(achievementId, data);
|
|
5512
|
+
return client.achievements.update(achievementId, withoutRedactedToken(data));
|
|
5466
5513
|
}
|
|
5467
5514
|
async function deleteAchievementConfig(client, achievementId) {
|
|
5468
5515
|
validateResourceId(achievementId, "achievement");
|
|
@@ -5494,11 +5541,11 @@ async function getLeaderboardConfig(client, leaderboardId) {
|
|
|
5494
5541
|
}
|
|
5495
5542
|
async function createLeaderboardConfig(client, applicationId, data) {
|
|
5496
5543
|
validateApplicationId(applicationId);
|
|
5497
|
-
return client.leaderboards.insert(applicationId, data);
|
|
5544
|
+
return client.leaderboards.insert(applicationId, withoutRedactedToken(data));
|
|
5498
5545
|
}
|
|
5499
5546
|
async function updateLeaderboardConfig(client, leaderboardId, data) {
|
|
5500
5547
|
validateResourceId(leaderboardId, "leaderboard");
|
|
5501
|
-
return client.leaderboards.update(leaderboardId, data);
|
|
5548
|
+
return client.leaderboards.update(leaderboardId, withoutRedactedToken(data));
|
|
5502
5549
|
}
|
|
5503
5550
|
async function deleteLeaderboardConfig(client, leaderboardId) {
|
|
5504
5551
|
validateResourceId(leaderboardId, "leaderboard");
|
|
@@ -5556,6 +5603,11 @@ async function setLeaderboardIcon(client, leaderboardId, filePath) {
|
|
|
5556
5603
|
iconContentType(filePath)
|
|
5557
5604
|
);
|
|
5558
5605
|
}
|
|
5606
|
+
function withoutRedactedToken(config) {
|
|
5607
|
+
if (config.token !== REDACTED) return config;
|
|
5608
|
+
const { token: _token, ...rest } = config;
|
|
5609
|
+
return rest;
|
|
5610
|
+
}
|
|
5559
5611
|
function safeConfigFilename(id) {
|
|
5560
5612
|
return `${id.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`;
|
|
5561
5613
|
}
|
|
@@ -5583,7 +5635,7 @@ async function readConfigDir(dir) {
|
|
|
5583
5635
|
"Each file must contain a single achievement or leaderboard config object."
|
|
5584
5636
|
);
|
|
5585
5637
|
}
|
|
5586
|
-
out.push({ file, config: parsed });
|
|
5638
|
+
out.push({ file, config: withoutRedactedToken(parsed) });
|
|
5587
5639
|
}
|
|
5588
5640
|
return out;
|
|
5589
5641
|
}
|
|
@@ -6293,6 +6345,7 @@ var SO_PATH_RE = /(?:^|\/)lib\/[^/]+\/[^/]+\.so$/;
|
|
|
6293
6345
|
var MODULE_MANIFEST_RE = /^([^/]+)\/manifest\/AndroidManifest\.xml$/;
|
|
6294
6346
|
var MAX_MODULE_MANIFEST_BYTES = 1024 * 1024;
|
|
6295
6347
|
var MAX_MODULE_MANIFESTS = 256;
|
|
6348
|
+
var MAX_SO_HEADER_READS = 512;
|
|
6296
6349
|
var SCAN_TIMEOUT_MS = 6e4;
|
|
6297
6350
|
function detectManifestPath(filePath) {
|
|
6298
6351
|
return filePath.toLowerCase().endsWith(".apk") ? APK_MANIFEST_PATH : AAB_MANIFEST_PATH;
|
|
@@ -6390,6 +6443,7 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
|
6390
6443
|
const soHeaders = /* @__PURE__ */ new Map();
|
|
6391
6444
|
const moduleManifests = /* @__PURE__ */ new Map();
|
|
6392
6445
|
let moduleManifestReads = 0;
|
|
6446
|
+
let soHeaderReads = 0;
|
|
6393
6447
|
let manifestBuf = null;
|
|
6394
6448
|
let pendingStreams = 0;
|
|
6395
6449
|
let entrysDone = false;
|
|
@@ -6406,6 +6460,10 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
|
6406
6460
|
settleResolve({ zipfile, entries, manifestBuf, soHeaders, moduleManifests });
|
|
6407
6461
|
}
|
|
6408
6462
|
}
|
|
6463
|
+
function advance() {
|
|
6464
|
+
if (settled) return;
|
|
6465
|
+
zipfile.readEntry();
|
|
6466
|
+
}
|
|
6409
6467
|
function readEntryStream(entry, onData, maxBytes, optional = false) {
|
|
6410
6468
|
pendingStreams++;
|
|
6411
6469
|
let done = false;
|
|
@@ -6417,6 +6475,7 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
|
6417
6475
|
if (done) return;
|
|
6418
6476
|
done = true;
|
|
6419
6477
|
pendingStreams--;
|
|
6478
|
+
advance();
|
|
6420
6479
|
maybeResolve();
|
|
6421
6480
|
};
|
|
6422
6481
|
zipfile.openReadStream(entry, (streamErr, stream) => {
|
|
@@ -6443,6 +6502,7 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
|
6443
6502
|
const buf = Buffer.concat(chunks);
|
|
6444
6503
|
onData(maxBytes ? buf.subarray(0, maxBytes) : buf);
|
|
6445
6504
|
pendingStreams--;
|
|
6505
|
+
advance();
|
|
6446
6506
|
maybeResolve();
|
|
6447
6507
|
};
|
|
6448
6508
|
stream.on("end", finish);
|
|
@@ -6479,7 +6539,8 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
|
6479
6539
|
MAX_MODULE_MANIFEST_BYTES,
|
|
6480
6540
|
true
|
|
6481
6541
|
);
|
|
6482
|
-
} else if (SO_PATH_RE.test(path)) {
|
|
6542
|
+
} else if (SO_PATH_RE.test(path) && soHeaderReads < MAX_SO_HEADER_READS) {
|
|
6543
|
+
soHeaderReads++;
|
|
6483
6544
|
readEntryStream(
|
|
6484
6545
|
entry,
|
|
6485
6546
|
(buf) => {
|
|
@@ -6487,8 +6548,9 @@ function openAndScan(buffer, manifestPath = AAB_MANIFEST_PATH) {
|
|
|
6487
6548
|
},
|
|
6488
6549
|
SO_HEADER_BYTES
|
|
6489
6550
|
);
|
|
6551
|
+
} else {
|
|
6552
|
+
advance();
|
|
6490
6553
|
}
|
|
6491
|
-
zipfile.readEntry();
|
|
6492
6554
|
} catch (e) {
|
|
6493
6555
|
fail(e instanceof Error ? e : new Error(String(e)));
|
|
6494
6556
|
}
|
|
@@ -7044,6 +7106,20 @@ var nativeLibsScanner = {
|
|
|
7044
7106
|
});
|
|
7045
7107
|
}
|
|
7046
7108
|
check16KBAlignment(ctx.nativeLibHeaders, ctx.manifest, findings);
|
|
7109
|
+
if (ctx.nativeLibHeaders) {
|
|
7110
|
+
const soCount = entries.filter((e) => nativeLibAbi(e.path) && e.path.endsWith(".so")).length;
|
|
7111
|
+
const unchecked = soCount - ctx.nativeLibHeaders.size;
|
|
7112
|
+
if (unchecked > 0) {
|
|
7113
|
+
findings.push({
|
|
7114
|
+
scanner: "native-libs",
|
|
7115
|
+
ruleId: "native-libs-16kb-unchecked",
|
|
7116
|
+
severity: "warning",
|
|
7117
|
+
title: `${unchecked} of ${soCount} native libraries not checked for 16KB alignment`,
|
|
7118
|
+
message: `Preflight checked the first ${ctx.nativeLibHeaders.size} native libraries in this bundle; the remaining ${unchecked} were not inspected, so a misaligned library among them would not be reported.`,
|
|
7119
|
+
suggestion: "Verify alignment of the full bundle with Android Studio's APK Analyzer or `zipalign -c -P 16 -v 4 app.apk` on a universal APK."
|
|
7120
|
+
});
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7047
7123
|
const detectedAbis = KNOWN_ABIS.filter((abi) => abisFound.has(abi));
|
|
7048
7124
|
const unknownAbis = [...abisFound].filter(
|
|
7049
7125
|
(abi) => !KNOWN_ABIS.includes(abi)
|
|
@@ -9807,7 +9883,7 @@ async function sha256File(filePath) {
|
|
|
9807
9883
|
}
|
|
9808
9884
|
|
|
9809
9885
|
// src/commands/image-sync.ts
|
|
9810
|
-
import { readdir as readdir7
|
|
9886
|
+
import { readdir as readdir7 } from "fs/promises";
|
|
9811
9887
|
import { join as join13, extname as extname5 } from "path";
|
|
9812
9888
|
import { PlayApiError } from "@gpc-cli/api";
|
|
9813
9889
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg"]);
|
|
@@ -9822,12 +9898,20 @@ var ALL_IMAGE_TYPES2 = [
|
|
|
9822
9898
|
"wearScreenshots"
|
|
9823
9899
|
];
|
|
9824
9900
|
async function scanLocalImages(dir) {
|
|
9901
|
+
let entries;
|
|
9825
9902
|
try {
|
|
9826
|
-
|
|
9827
|
-
|
|
9828
|
-
|
|
9829
|
-
return
|
|
9903
|
+
entries = await readdir7(dir, { withFileTypes: true });
|
|
9904
|
+
} catch (error) {
|
|
9905
|
+
const code = error.code;
|
|
9906
|
+
if (code === "ENOENT" || code === "ENOTDIR") return null;
|
|
9907
|
+
throw new GpcError(
|
|
9908
|
+
`Cannot read image directory "${dir}": ${code ?? String(error)}`,
|
|
9909
|
+
"IMAGE_SYNC_DIR_UNREADABLE",
|
|
9910
|
+
1,
|
|
9911
|
+
"Check the directory's permissions (and container volume ownership in CI). Nothing was changed on Google Play."
|
|
9912
|
+
);
|
|
9830
9913
|
}
|
|
9914
|
+
return entries.filter((e) => e.isFile() && IMAGE_EXTENSIONS.has(extname5(e.name).toLowerCase())).map((e) => e.name).sort();
|
|
9831
9915
|
}
|
|
9832
9916
|
async function scanLanguages(dir) {
|
|
9833
9917
|
try {
|
|
@@ -9837,13 +9921,6 @@ async function scanLanguages(dir) {
|
|
|
9837
9921
|
return [];
|
|
9838
9922
|
}
|
|
9839
9923
|
}
|
|
9840
|
-
async function dirExists(path) {
|
|
9841
|
-
try {
|
|
9842
|
-
return (await stat10(path)).isDirectory();
|
|
9843
|
-
} catch {
|
|
9844
|
-
return false;
|
|
9845
|
-
}
|
|
9846
|
-
}
|
|
9847
9924
|
async function syncImages(client, packageName, dir, options) {
|
|
9848
9925
|
const details = [];
|
|
9849
9926
|
let uploaded = 0;
|
|
@@ -9864,7 +9941,8 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
9864
9941
|
for (const language of languages) {
|
|
9865
9942
|
for (const imageType of imageTypes) {
|
|
9866
9943
|
const localDir = join13(dir, language, imageType);
|
|
9867
|
-
const
|
|
9944
|
+
const scanned = await scanLocalImages(localDir);
|
|
9945
|
+
const localFiles = scanned ?? [];
|
|
9868
9946
|
let remoteImages;
|
|
9869
9947
|
try {
|
|
9870
9948
|
remoteImages = await client.images.list(packageName, edit.id, language, imageType);
|
|
@@ -9906,7 +9984,7 @@ async function syncImages(client, packageName, dir, options) {
|
|
|
9906
9984
|
details.push({ language, imageType, file, action: "upload", reason });
|
|
9907
9985
|
};
|
|
9908
9986
|
if (options?.delete) {
|
|
9909
|
-
if (
|
|
9987
|
+
if (scanned === null && remoteImages.length > 0) {
|
|
9910
9988
|
for (const img of remoteImages) {
|
|
9911
9989
|
details.push({
|
|
9912
9990
|
language,
|