mirror of
https://github.com/Crosstalk-Solutions/project-nomad.git
synced 2026-10-16 21:00:03 +02:00
Merge e3beb63182 into ee203df485
This commit is contained in:
commit
ebf298049a
|
|
@ -9,6 +9,9 @@ import { DownloadJobWithProgress, DownloadProgressData, RunDownloadJobParams } f
|
|||
import type { Job, Queue } from 'bullmq'
|
||||
import { normalize } from 'path'
|
||||
import { deleteFileIfExists } from '../utils/fs.js'
|
||||
import { refreshRetryParams } from '../utils/download_retry.js'
|
||||
import { CollectionManifestService } from './collection_manifest_service.js'
|
||||
import type { ZimCategoriesSpec } from '../../types/collections.js'
|
||||
import transmit from '@adonisjs/transmit/services/main'
|
||||
import { BROADCAST_CHANNELS } from '../../constants/broadcast.js'
|
||||
|
||||
|
|
@ -200,11 +203,13 @@ export class DownloadService {
|
|||
return { success: true, message: `Retrying download for model ${modelName}` }
|
||||
}
|
||||
|
||||
// For file downloads (zim, map, etc.), re-dispatch with original params
|
||||
const params = job.data as RunDownloadJobParams
|
||||
if (!params.url || !params.filepath) {
|
||||
// For file downloads (zim, map, etc.), re-dispatch with the stored params,
|
||||
// refreshed from the current catalog for a curated ZIM
|
||||
const stored = job.data as RunDownloadJobParams
|
||||
if (!stored.url || !stored.filepath) {
|
||||
return { success: false, message: 'Cannot retry: missing URL or filepath in job data' }
|
||||
}
|
||||
const params = refreshRetryParams(stored, await this.currentZimCatalog())
|
||||
|
||||
// Remove the old failed job, then dispatch a fresh one
|
||||
await job.remove().catch(() => {})
|
||||
|
|
@ -216,6 +221,17 @@ export class DownloadService {
|
|||
return { success: false, message: 'Failed job not found. It may have already been dismissed.' }
|
||||
}
|
||||
|
||||
/** Null when the catalog cannot be read, so a retry falls back to its stored params. */
|
||||
private async currentZimCatalog(): Promise<ZimCategoriesSpec | null> {
|
||||
try {
|
||||
return await new CollectionManifestService().getSpecWithFallback<ZimCategoriesSpec>(
|
||||
'zim_categories'
|
||||
)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async cancelJob(jobId: string): Promise<{ success: boolean; message: string }> {
|
||||
const queue = this.queueService.getQueue(RunDownloadJob.queue)
|
||||
const job = await queue.getJob(jobId)
|
||||
|
|
|
|||
38
admin/app/utils/download_retry.ts
Normal file
38
admin/app/utils/download_retry.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { dirname, join } from 'node:path'
|
||||
|
||||
import type { RunDownloadJobParams } from '../../types/downloads.js'
|
||||
import type { ZimCategoriesSpec } from '../../types/collections.js'
|
||||
|
||||
/**
|
||||
* Params for retrying a failed download. A job's data is frozen when the job
|
||||
* is created, so a curated ZIM whose catalog entry has since been corrected
|
||||
* (a dead URL replaced, a new build date) is retried with what the catalog
|
||||
* says now. The filename changes with the URL because ZIM filenames carry the
|
||||
* build date.
|
||||
*
|
||||
* Anything else keeps its stored params: a download that came from no
|
||||
* collection, a non-ZIM download, and a resource the catalog no longer lists
|
||||
* (removed or renamed), so a retry is never worse than re-sending the original.
|
||||
*/
|
||||
export function refreshRetryParams(
|
||||
params: RunDownloadJobParams,
|
||||
spec: ZimCategoriesSpec | null
|
||||
): RunDownloadJobParams {
|
||||
const metadata = params.resourceMetadata
|
||||
if (!spec || params.filetype !== 'zim' || !metadata?.collection_ref) return params
|
||||
|
||||
const current = spec.categories
|
||||
.flatMap((category) => category.tiers)
|
||||
.flatMap((tier) => tier.resources)
|
||||
.find((resource) => resource.id === metadata.resource_id && resource.type !== 'dataset')
|
||||
const filename = current?.url.split('/').pop()
|
||||
if (!current || !filename) return params
|
||||
|
||||
return {
|
||||
...params,
|
||||
url: current.url,
|
||||
filepath: join(dirname(params.filepath), filename),
|
||||
totalBytes: current.size_mb > 0 ? current.size_mb * 1024 * 1024 : params.totalBytes,
|
||||
resourceMetadata: { ...metadata, version: current.version },
|
||||
}
|
||||
}
|
||||
115
admin/tests/unit/download_retry.spec.ts
Normal file
115
admin/tests/unit/download_retry.spec.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import * as assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import { refreshRetryParams } from '../../app/utils/download_retry.js'
|
||||
import type { RunDownloadJobParams } from '../../types/downloads.js'
|
||||
import type { ZimCategoriesSpec } from '../../types/collections.js'
|
||||
|
||||
const failed: RunDownloadJobParams = {
|
||||
url: 'https://download.kiwix.org/zim/other/prepper_en_2025-01.zim',
|
||||
filepath: '/app/storage/zim/prepper_en_2025-01.zim',
|
||||
timeout: 30000,
|
||||
allowedMimeTypes: ['application/x-zim'],
|
||||
filetype: 'zim',
|
||||
title: 'Prepper',
|
||||
totalBytes: 100 * 1024 * 1024,
|
||||
resourceMetadata: {
|
||||
resource_id: 'prepper_en',
|
||||
version: '2025-01',
|
||||
collection_ref: 'survival',
|
||||
},
|
||||
}
|
||||
|
||||
function specWith(resource: { id: string; version: string; url: string; size_mb: number }) {
|
||||
return {
|
||||
spec_version: '1',
|
||||
categories: [
|
||||
{
|
||||
name: 'Survival',
|
||||
slug: 'survival',
|
||||
icon: 'tent',
|
||||
description: '',
|
||||
language: 'en',
|
||||
tiers: [
|
||||
{
|
||||
name: 'Essential',
|
||||
slug: 'essential',
|
||||
description: '',
|
||||
resources: [{ title: 'Prepper', description: '', ...resource }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} satisfies ZimCategoriesSpec
|
||||
}
|
||||
|
||||
test('retry of a curated download uses the url, version and filename the catalog has now', () => {
|
||||
const retried = refreshRetryParams(
|
||||
failed,
|
||||
specWith({
|
||||
id: 'prepper_en',
|
||||
version: '2026-09',
|
||||
url: 'https://download.kiwix.org/zim/other/prepper_en_2026-09.zim',
|
||||
size_mb: 120,
|
||||
})
|
||||
)
|
||||
|
||||
assert.equal(retried.url, 'https://download.kiwix.org/zim/other/prepper_en_2026-09.zim')
|
||||
assert.equal(retried.filepath, '/app/storage/zim/prepper_en_2026-09.zim')
|
||||
assert.equal(retried.totalBytes, 120 * 1024 * 1024)
|
||||
assert.deepEqual(retried.resourceMetadata, {
|
||||
resource_id: 'prepper_en',
|
||||
version: '2026-09',
|
||||
collection_ref: 'survival',
|
||||
})
|
||||
assert.equal(retried.title, 'Prepper')
|
||||
})
|
||||
|
||||
test('retry keeps the stored params when the catalog no longer has the resource', () => {
|
||||
const retried = refreshRetryParams(
|
||||
failed,
|
||||
specWith({
|
||||
id: 'prepper_en_renamed',
|
||||
version: '2026-09',
|
||||
url: 'https://download.kiwix.org/zim/other/prepper_en_renamed_2026-09.zim',
|
||||
size_mb: 120,
|
||||
})
|
||||
)
|
||||
|
||||
assert.deepEqual(retried, failed)
|
||||
})
|
||||
|
||||
test('retry keeps the stored params when no catalog is available', () => {
|
||||
assert.deepEqual(refreshRetryParams(failed, null), failed)
|
||||
})
|
||||
|
||||
test('retry of a download that came from no collection keeps the stored params', () => {
|
||||
const manual: RunDownloadJobParams = {
|
||||
...failed,
|
||||
resourceMetadata: { resource_id: 'prepper_en', version: '2025-01', collection_ref: null },
|
||||
}
|
||||
const spec = specWith({
|
||||
id: 'prepper_en',
|
||||
version: '2026-09',
|
||||
url: 'https://download.kiwix.org/zim/other/prepper_en_2026-09.zim',
|
||||
size_mb: 120,
|
||||
})
|
||||
|
||||
assert.deepEqual(refreshRetryParams(manual, spec), manual)
|
||||
assert.deepEqual(refreshRetryParams({ ...failed, resourceMetadata: undefined }, spec), {
|
||||
...failed,
|
||||
resourceMetadata: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
test('retry of a map download keeps the stored params', () => {
|
||||
const map: RunDownloadJobParams = { ...failed, filetype: 'map' }
|
||||
const spec = specWith({
|
||||
id: 'prepper_en',
|
||||
version: '2026-09',
|
||||
url: 'https://download.kiwix.org/zim/other/prepper_en_2026-09.zim',
|
||||
size_mb: 120,
|
||||
})
|
||||
|
||||
assert.deepEqual(refreshRetryParams(map, spec), map)
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user