vomebook commited on
Commit
58fbb91
·
verified ·
1 Parent(s): 793dac0

Preserve exact original download filenames

Browse files
backend/record_metadata.py CHANGED
@@ -172,6 +172,9 @@ def reader_resolved_record(record: dict, asset: dict) -> dict:
172
  return {
173
  "url": source, "download": source, "extension": extension,
174
  "original_extension": extension, "title": record.get("File") or record.get("Name") or "在线阅读",
 
 
 
175
  "path": "/".join([repo, *folders]), "folder": "/".join(folders), "repo": repo,
176
  **reader_asset_fields(asset, extension),
177
  }
 
172
  return {
173
  "url": source, "download": source, "extension": extension,
174
  "original_extension": extension, "title": record.get("File") or record.get("Name") or "在线阅读",
175
+ # Keep the exact indexed filename separate from the display title.
176
+ # The stem may itself end in an extension, or include trailing spaces.
177
+ "filename": build_record_relative_path(record).rsplit("/", 1)[-1],
178
  "path": "/".join([repo, *folders]), "folder": "/".join(folders), "repo": repo,
179
  **reader_asset_fields(asset, extension),
180
  }
static/app.js CHANGED
@@ -2546,7 +2546,7 @@ function getReaderLink(rec, returnUrl = location.href, cacheMetadata = true) {
2546
  readerRecord.OcrUrl = `/txt/${encodeRecordPath(VoiceOfMLReader.txtRelativePath(relPath))}`;
2547
  }
2548
  const readerUrl = VoiceOfMLReader.readerUrl(readerRecord, "/static/reader.html");
2549
- if (cacheMetadata) try { const readerId = new URL(readerUrl, location.origin).searchParams.get("id"); if (readerId) { const sourceData = { url: readerRecord.ReaderLink || readerRecord.Link, download: readerRecord.Link, title: readerRecord.File, extension: readerRecord.ReaderExtension || readerRecord.Extension, original_extension: readerRecord.Extension, repo: String(readerRecord.Repo || "").split("/").pop(), folder: readerRecord.Folder, chapter_manifest: readerRecord.ReaderChapterManifest || "", pdf_document: readerRecord.ReaderPdfDocument || "" }; cacheReaderMetadata(readerId, sourceData, true); } } catch (_) {}
2550
  return readerUrl;
2551
  }
2552
 
 
2546
  readerRecord.OcrUrl = `/txt/${encodeRecordPath(VoiceOfMLReader.txtRelativePath(relPath))}`;
2547
  }
2548
  const readerUrl = VoiceOfMLReader.readerUrl(readerRecord, "/static/reader.html");
2549
+ if (cacheMetadata) try { const readerId = new URL(readerUrl, location.origin).searchParams.get("id"); if (readerId) { const sourceData = { url: readerRecord.ReaderLink || readerRecord.Link, download: readerRecord.Link, title: readerRecord.File, filename: String(readerRecord.File || "") + (readerRecord.Extension ? "." + readerRecord.Extension : ""), extension: readerRecord.ReaderExtension || readerRecord.Extension, original_extension: readerRecord.Extension, repo: String(readerRecord.Repo || "").split("/").pop(), folder: readerRecord.Folder, chapter_manifest: readerRecord.ReaderChapterManifest || "", pdf_document: readerRecord.ReaderPdfDocument || "" }; cacheReaderMetadata(readerId, sourceData, true); } } catch (_) {}
2550
  return readerUrl;
2551
  }
2552
 
static/reader/core/reader-source.mjs CHANGED
@@ -18,8 +18,16 @@ export function createReaderSource(readerContext) {
18
  return path;
19
  }
20
 
 
 
 
 
 
 
 
 
21
  function readerDownloadUrl(filename, link) {
22
- return readerContext.readerDownloadApiUrl(`/api/download?file=${encodeURIComponent(filename || "file")}&link=${encodeURIComponent(link || "")}`);
23
  }
24
 
25
  async function startReaderDownload(event) {
 
18
  return path;
19
  }
20
 
21
+ function readerOriginalFilename(fallback) {
22
+ // The human-readable title can omit a repeated suffix or normalize spaces;
23
+ // the authoritative metadata keeps the exact source filename separately.
24
+ const name = readerContext.resolvedReaderData?.filename;
25
+ return typeof name === "string" && name.trim() && !/[\/\\\x00-\x1f\x7f]/.test(name)
26
+ ? name : fallback;
27
+ }
28
+
29
  function readerDownloadUrl(filename, link) {
30
+ return readerContext.readerDownloadApiUrl(`/api/download?file=${encodeURIComponent(readerOriginalFilename(filename || "file"))}&link=${encodeURIComponent(link || "")}`);
31
  }
32
 
33
  async function startReaderDownload(event) {
tests/test_fastapi_endpoints.py CHANGED
@@ -409,9 +409,17 @@ class FastApiEndpointTests(unittest.TestCase):
409
  response=self.client.get('/api/reader-resolve',params={'id':'2w2m8oowhjtl9'})
410
  self.assertEqual(response.status_code,200)
411
  self.assertTrue(response.json()['download'].endswith('/www.maopost.com_postl_0566_001M.jpg.jpg'))
 
412
  self.assertEqual(response.json()['extension'],'webp')
413
  self.assertIn(asset['p'],response.json()['url'])
414
 
 
 
 
 
 
 
 
415
  def test_legacy_double_extension_alias_collision_fails_closed(self):
416
  app.records=[{"Repo":"VoiceOfML/Test", "File":name, "Extension":"jpg", "Folder":[]}
417
  for name in ('one.jpg','two.jpg')]
 
409
  response=self.client.get('/api/reader-resolve',params={'id':'2w2m8oowhjtl9'})
410
  self.assertEqual(response.status_code,200)
411
  self.assertTrue(response.json()['download'].endswith('/www.maopost.com_postl_0566_001M.jpg.jpg'))
412
+ self.assertEqual(response.json()['filename'],'www.maopost.com_postl_0566_001M.jpg.jpg')
413
  self.assertEqual(response.json()['extension'],'webp')
414
  self.assertIn(asset['p'],response.json()['url'])
415
 
416
+ def test_resolved_filename_preserves_case_spaces_and_repeated_extension(self):
417
+ for stem, extension in [('poster.jpg','jpg'),('Lecture','PDF'),('Book ','pdf'),('README','')]:
418
+ with self.subTest(stem=stem,extension=extension):
419
+ record={'Repo':'VoiceOfML/Test','File':stem,'Extension':extension,'Folder':['docs']}
420
+ data=app.reader_resolved_record(record,{})
421
+ self.assertEqual(data['filename'],stem+('.'+extension if extension else ''))
422
+
423
  def test_legacy_double_extension_alias_collision_fails_closed(self):
424
  app.records=[{"Repo":"VoiceOfML/Test", "File":name, "Extension":"jpg", "Folder":[]}
425
  for name in ('one.jpg','two.jpg')]
tests/test_reader_navigation_host.py CHANGED
@@ -110,6 +110,14 @@ process.stdout.write(stripBundledScripts(readFileSync('static/index.html','utf8'
110
  self.assertTrue(row['link'].endswith('/目录/'+row['filename']))
111
  self.assertTrue(result[0]['reader'])
112
 
 
 
 
 
 
 
 
 
113
  def test_explicit_close_ignores_late_panel_state_before_history_pop(self):
114
  self.page.evaluate('navigateToReader("/static/reader.html?ext=txt")')
115
  self.page.frame_locator('.reader-overlay').locator('body').wait_for()
 
110
  self.assertTrue(row['link'].endswith('/目录/'+row['filename']))
111
  self.assertTrue(result[0]['reader'])
112
 
113
+ def test_session_handoff_keeps_exact_original_filename(self):
114
+ result=self.page.evaluate('''()=>{
115
+ const record={Repo:'VoiceOfML/Test',Folder:['docs'],File:'poster.jpg',Extension:'JPG'};
116
+ const url=getReaderLink(record),id=new URL(url,location.href).searchParams.get('id');
117
+ return JSON.parse(sessionStorage.getItem('reader-source:'+id));
118
+ }''')
119
+ self.assertEqual(result['filename'],'poster.jpg.JPG')
120
+
121
  def test_explicit_close_ignores_late_panel_state_before_history_pop(self):
122
  self.page.evaluate('navigateToReader("/static/reader.html?ext=txt")')
123
  self.page.frame_locator('.reader-overlay').locator('body').wait_for()
tests/test_reader_pdf_loading.py CHANGED
@@ -60,6 +60,48 @@ class PdfLoadingTests(unittest.TestCase):
60
  self.assertEqual(len(self.modules), 1)
61
  self.assertNotIn('reader-module-retry', self.modules[0])
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  def test_first_range_overlaps_held_engine_and_is_consumed_once(self):
64
  held = []
65
  self.page.route(MODULE, lambda route: held.append(route))
 
60
  self.assertEqual(len(self.modules), 1)
61
  self.assertNotIn('reader-module-retry', self.modules[0])
62
 
63
+ def test_resolved_original_download_name_keeps_exact_repeated_extension(self):
64
+ original='https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/poster.jpg.jpg'
65
+ image=b'GIF89a\x01\x00\x01\x00\x80\x00\x00\x00\x00\x00\xff\xff\xff!\xf9\x04\x01\x00\x00\x00\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x02D\x01\x00;'
66
+ self.page.route('**/api/reader-resolve?id=filename-test',lambda route:route.fulfill(json={
67
+ 'url':original,'download':original,'extension':'jpg','original_extension':'jpg',
68
+ 'title':'poster.jpg','filename':'poster.jpg.jpg'}))
69
+ self.page.route('**/api/reader-content**',lambda route:route.fulfill(content_type='image/gif',body=image))
70
+ self.page.goto(self.origin+'/static/reader.html?id=filename-test&ext=jpg',wait_until='domcontentloaded')
71
+ self.page.locator('.reader-image').wait_for()
72
+ query=urllib.parse.parse_qs(urllib.parse.urlsplit(self.page.locator('#download').get_attribute('href')).query)
73
+ self.assertEqual(query['file'],['poster.jpg.jpg'])
74
+ self.assertEqual(query['link'],[original])
75
+
76
+ def test_malformed_original_filename_metadata_is_not_used_for_download(self):
77
+ source='https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/safe.txt'
78
+ self.page.route('**/api/reader-resolve?id=filename-invalid',lambda route:route.fulfill(json={
79
+ 'url':source,'download':source,'extension':'txt','original_extension':'txt',
80
+ 'title':'safe','filename':'../unsafe.txt'}))
81
+ self.page.route('**/api/reader-content**',lambda route:route.fulfill(content_type='text/plain',body='safe'))
82
+ self.page.goto(self.origin+'/static/reader.html?id=filename-invalid&ext=txt',wait_until='domcontentloaded')
83
+ self.page.locator('.reader-text').wait_for()
84
+ query=urllib.parse.parse_qs(urllib.parse.urlsplit(self.page.locator('#download').get_attribute('href')).query)
85
+ self.assertEqual(query['file'],['safe.txt'])
86
+
87
+ def test_original_download_name_survives_title_change_and_button_handoff(self):
88
+ source='https://huggingface.co/datasets/VoiceOfML/Test/resolve/main/Sample%20.PDF'
89
+ checks=[]
90
+ self.page.route('**/api/reader-resolve?id=filename-click',lambda route:route.fulfill(json={
91
+ 'url':source,'download':source,'extension':'txt','original_extension':'pdf',
92
+ 'title':'Sample ','filename':'Sample .PDF'}))
93
+ self.page.route('**/api/reader-content**',lambda route:route.fulfill(content_type='text/plain',body='safe'))
94
+ self.page.route('**/api/download/check?**',lambda route:route.fulfill(json={'ok':True}))
95
+ self.page.route('**/api/download?**',lambda route:(checks.append(route.request.url),route.fulfill(body='file')))
96
+ self.page.goto(self.origin+'/static/reader.html?id=filename-click&ext=txt',wait_until='domcontentloaded')
97
+ self.page.locator('.reader-text').wait_for()
98
+ self.page.locator('#download').click()
99
+ self.page.wait_for_function("()=>document.querySelector('#download-feedback')?.textContent.includes('已发起下载')")
100
+ self.assertEqual(len(checks),1)
101
+ query=urllib.parse.parse_qs(urllib.parse.urlsplit(checks[0]).query)
102
+ self.assertEqual(query['file'],['Sample .PDF'])
103
+ self.assertEqual(query['link'],[source])
104
+
105
  def test_first_range_overlaps_held_engine_and_is_consumed_once(self):
106
  held = []
107
  self.page.route(MODULE, lambda route: held.append(route))