Skip to content

Commit b5917f2

Browse files
committed
v1.0.6 — Einzeldatei-Download entfernen (UI-Aufräumen)
Entfernt: "Download"-Button im "Dateien anzeigen"-Panel, SBU_Plugin::ajax_download(), SBU_Seafile_API::download_file(), DEFAULT_DOWNLOAD_CHUNK-Konstante, window.sDl und der download-file-Event-Delegation-Eintrag, Hook-Registrierung 'download'. Grund: Der Button hieß "Download", triggerte aber keinen Browser-Download — er kopierte die Datei serverseitig ins UpdraftPlus-Verzeichnis und schrieb die Erfolgsmeldung ins gemeinsame #sr-Statusfeld (außerhalb des Sichtbereichs bei gescrollter Seite). Der vorgelagerte confirm(f)-Dialog zeigte nur den rohen Dateinamen ohne Erklärung. Zusammen führte das zum "nichts passiert"-Eindruck. Warum entfernt statt repariert: UpdraftPlus-Chunks (uploads7.zip, uploads14.zip, ...) sind semantisch undurchsichtig — eine einzelne Datei aus dem Set ist ohne Kontext nicht restore-fähig. Der bestehende "Wiederherstellen"-Pfad nutzt bereits "Teilweise lokal"-Logik und zieht fehlende Dateien gezielt nach; für forensische Einzeldatei-Inspektion ist die Seafile-Weboberfläche der direktere Weg. Kein realistischer Use-Case für den Button. Die im Restore-Flow aktive Download-Mechanik (SBU_Seafile_API::download_whole_file_stream + download_chunks_parallel) bleibt unverändert. get_download_link ist weiterhin mehrfach vom Restore-Flow aus aufgerufen. Übersetzungsvorlage regeneriert. README, ARCHITECTURE und CONTRIBUTING angepasst (22 Admin-AJAX-Handler, one-file restore aus Test-Checkliste gestrichen). 121 Tests / 333 Assertions, alle Gates grün.
1 parent 279a2c9 commit b5917f2

10 files changed

Lines changed: 239 additions & 845 deletions

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,37 @@ Alle relevanten Änderungen an diesem Plugin werden in dieser Datei dokumentiert
44

55
Format nach [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), Versionierung nach [Semantic Versioning](https://semver.org/lang/de/).
66

7+
## [1.0.6] — 2026-04-19
8+
9+
UI-Aufräumen: Einzeldatei-Download aus der Backup-Liste entfernt. Kein Feature-Verlust, nur weniger Verwirrung.
10+
11+
### Entfernt
12+
13+
- **Einzeldatei-Download-Button im „Dateien anzeigen"-Panel.** Der Knopf hieß „Download", triggerte aber **keinen** Browser-Download: `SBU_Plugin::ajax_download()` lud die Datei serverseitig von Seafile in den lokalen UpdraftPlus-Ordner und schrieb die Erfolgsmeldung in das gemeinsam genutzte `#sr`-Statusfeld. Das Feld liegt zwei Panels höher — bei gescrollter Seite außerhalb des Sichtbereichs, weshalb der Pfad sich wie „nichts passiert" anfühlte. Zusätzlich fragte der vorgelagerte `confirm(f)`-Dialog nur den rohen Dateinamen ab (z. B. `backup_2026-04-17-1630_INTERIORISTA_1d2b1dcea52d-db.gz`) ohne zu erklären, was die Bestätigung auslöst.
14+
- **Rationale für die Entfernung statt Umbau zum Browser-Download:**
15+
- **Kein Use-Case:** UpdraftPlus erzeugt die Backup-Sets als nummerierte Chunks (`uploads7.zip`, `uploads14.zip`, …) ohne semantische Bedeutung der Chunk-Nummer. Eine einzelne Datei aus dem Set ist für den Nutzer blind — ohne das vollständige Set ist sie nicht restore-fähig.
16+
- **„Wiederherstellen" deckt den Restore-Fall ab:** Der bestehende Restore-Pfad nutzt bereits die „Teilweise lokal"-Erkennung und zieht genau die fehlenden Dateien nach. Das ist der gezielte Re-Download, den ein Einzeldatei-Button theoretisch bieten könnte.
17+
- **Seafile-Weboberfläche deckt den Inspektions-Fall ab:** Wer eine einzelne `.gz` außerhalb von WordPress untersuchen will, lädt sie direkt aus der Seafile-Weboberfläche — ein Plugin-Umweg bringt dort keinen Mehrwert.
18+
- **Jeder Button, der nicht verstanden wird, ist eine Support-Quelle.** Die aktuelle Plugin-Zielgruppe (self-hoster, der UpdraftPlus + Seafile koppelt) klickt ihn in der Praxis versehentlich und wundert sich.
19+
20+
### Aufgeräumt (tote Code-Pfade)
21+
22+
- **`SBU_Plugin::ajax_download()`** — Handler vollständig entfernt (inkl. der `'Einzelne Datei heruntergeladen: …'`- und `'Ungültige Parameter.'`-Strings, die nur hier vorkamen).
23+
- **`'download'` aus der AJAX-Hook-Registrierung in `SBU_Plugin::boot_plugin()`** — damit ist die `wp_ajax_sbu_download`-Route weg. Wer die Route manuell aufruft, bekommt jetzt WordPress' Standard-0-Response.
24+
- **`SBU_Seafile_API::download_file()`** — die nur von `ajax_download()` aufgerufene Methode (Range-Chunk-Download mit Link-Refresh-Retry) ist jetzt tot und wurde entfernt. Sie überschnitt sich funktional mit dem aktiven Restore-Pfad aus `SBU_Seafile_API::download_whole_file_stream()` + `download_chunks_parallel()`, die der Restore-Flow nutzt — die bleiben unverändert.
25+
- **`SBU_Seafile_API::DEFAULT_DOWNLOAD_CHUNK`-Konstante** — nur von `download_file()` referenziert, mit der Methode entfernt. Die im Restore-Flow aktive Standard-Chunk-Größe steckt in `SBU_DOWNLOAD_CHUNK_MB_DEFAULT` (20 MB) und bleibt so wie sie war.
26+
- **`window.sDl`** und **`'download-file'` im Event-Delegation-Map** in `assets/js/admin.js` — entfernt.
27+
- **`<button data-sbu-action="download-file">` im „Dateien anzeigen"-Panel** in `SBU_Plugin::ajax_list()` — entfernt. Das Panel zeigt jetzt nur noch Dateiname und Größe pro Zeile, reine Inhaltsliste ohne Aktionen.
28+
29+
### Geändert
30+
31+
- **Übersetzungsvorlage (`languages/seafile-updraft-backup-uploader.pot`)** — neu generiert. Die Strings des entfernten Pfads sind nicht mehr Teil der Vorlage. Die Regeneration hat zusätzlich die alte Version-Referenz (aus einem früheren Versionszweig) auf den aktuellen Stand gezogen.
32+
- **Dokumentation**`README.md`, `ARCHITECTURE.md` und `CONTRIBUTING.md` auf den neuen Admin-AJAX-Handler-Stand (22 Admin-Endpunkte + 1 öffentlicher Cron-Endpoint) aktualisiert, „one-file restore" aus der Test-Checkliste gestrichen.
33+
34+
### Tests
35+
36+
**121 Tests / 333 Assertions** — unverändert. PHPCS, PHPStan Level 5 und PHPUnit 11 grün auf PHP 8.2 / 8.3 / 8.4.
37+
738
## [1.0.5] — 2026-04-19
839

940
Breaking-Change-Release: PHP-Mindestanforderung auf 8.2 angehoben. Testsuite auf PHPUnit 11 gehoben und um zwei neue Abdeckungs-Schwerpunkte erweitert. Dokumentation (README, ARCHITECTURE, CONTRIBUTING) auf den aktuellen Code-Stand gezogen.

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ verify manually:
106106
`always` and fire for the right cases.
107107
- [ ] Dashboard widget displays the latest status.
108108
- [ ] Backup list loads and renders type badges.
109-
- [ ] Download from Seafile works (one-file restore and full restore).
109+
- [ ] Full restore from Seafile works (the only restore path; single-file
110+
downloads were removed in 1.0.6).
110111
- [ ] Delete on Seafile works.
111112
- [ ] Plugin uninstall removes all options.
112113

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ seafile-updraft-backup-uploader/
8787
│ ├── class-sbu-crypto.php — AES-256-CBC Password-Encryption
8888
│ ├── trait-sbu-upload-flow.php — Upload-Queue, Chunked-Upload, Retry/Backoff
8989
│ ├── trait-sbu-restore-flow.php — Restore-Queue, paralleler Range-Download
90-
│ └── trait-sbu-admin-ajax.php — 20 AJAX-Handler (Admin) + 1 öffentlicher Cron-Ping
90+
│ └── trait-sbu-admin-ajax.php — 22 AJAX-Handler (Admin) + 1 öffentlicher Cron-Ping
9191
├── views/
9292
│ └── admin-page.php — Template der Einstellungsseite
9393
├── assets/
@@ -113,7 +113,7 @@ seafile-updraft-backup-uploader/
113113
- `SBU_Crypto` — AES-256-CBC mit zufälligem IV pro Vorgang. Legacy-IV-Migration erkennt alte Formate und re-verschlüsselt beim nächsten Save.
114114
- `SBU_Upload_Flow` / `SBU_Restore_Flow` / `SBU_Admin_Ajax_Controller` — Traits, die die Upload-Queue-Mechanik, Restore-Queue-Mechanik und AJAX-Endpunkte in `SBU_Plugin` einhängen.
115115

116-
**Öffentliche Oberfläche:** 20 Admin-AJAX-Endpunkte (alle mit `manage_options` + Nonce) und genau **ein** öffentlicher Endpunkt `sbu_cron_ping` (per-site 32-char Secret-Key, `hash_equals()`-Vergleich).
116+
**Öffentliche Oberfläche:** 22 Admin-AJAX-Endpunkte (alle mit `manage_options` + Nonce) und genau **ein** öffentlicher Endpunkt `sbu_cron_ping` (per-site 32-char Secret-Key, `hash_equals()`-Vergleich).
117117

118118
Die Queue-Logik, das Locking-Modell und die State-Machine (uploading ↔ paused, → aborted/error/done) sind in [ARCHITECTURE.md](ARCHITECTURE.md) dokumentiert.
119119

assets/js/admin.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@
7878
function refreshLog(){P('sbu_get_log').then(function(d){if(d.success){var el=document.getElementById('alc');if(el){el.innerHTML=d.data||'<span class="dim">'+sbuAdmin.i18n.noActivity+'</span>';applyLogFilter();}}}).catch(function(){});}
7979

8080
window.sA=function(a,b){var e=document.getElementById('sr');e.className='ld';e.style.display='block';e.textContent=wait;if(b)b.disabled=true;showProgress(true);var extra=(a==='sbu_test')?getFormCreds()+'&sbu_lib='+encodeURIComponent(libSelect.value)+'&sbu_folder='+encodeURIComponent(folderSelect.value):'';P(a,extra).then(function(d){hideProgress();e.className=d.success?'ok':'er';e.innerHTML='<pre style="margin:0;white-space:pre-wrap;font-size:13px">'+(d.data||'')+'</pre>';if(b)b.disabled=false;if(a==='sbu_upload'){document.getElementById('sbu-upb').style.display='none';stallCount=0;loadBL(true);refreshLog();}if(a==='sbu_test'){loadBL(true);refreshLog();}}).catch(function(x){hideProgress();e.className='er';e.textContent=x.message||'Verbindungsfehler';if(b)b.disabled=false;if(a==='sbu_upload'){document.getElementById('sbu-upb').style.display='none';stallCount=0;}});};
81-
window.sDl=function(dir,f){if(!confirm(f))return;var e=document.getElementById('sr');e.className='ld';e.style.display='block';e.textContent='Download...';showProgress(true);P('sbu_download','&dir='+encodeURIComponent(dir)+'&file='+encodeURIComponent(f)).then(function(d){hideProgress();e.className=d.success?'ok':'er';e.textContent=d.data||'';refreshLog();}).catch(function(x){hideProgress();e.className='er';e.textContent=x.message;});};
8281
window.sDe=function(dir){if(!confirm(dir+'?'))return;var e=document.getElementById('sr');e.className='ld';e.style.display='block';e.textContent='...';P('sbu_delete','&dir='+encodeURIComponent(dir)).then(function(d){e.className=d.success?'ok':'er';e.textContent=d.data||'';if(d.success){loadBL(true);refreshLog();}});};
8382
window.sbuToggle=function(id,link){var el=document.getElementById(id);if(!el)return;var show=el.style.display==='none';el.style.display=show?'block':'none';link.textContent=show?sbuAdmin.i18n.hide:sbuAdmin.i18n.show;};
8483
window.sDlAll=function(dir){if(!confirm(sbuAdmin.i18n.restoreConfirm))return;var e=document.getElementById('sr');e.className='ld';e.style.display='block';e.textContent=sbuAdmin.i18n.downloadingAll;showProgress(true);P('sbu_download_all','&dir='+encodeURIComponent(dir)).then(function(d){hideProgress();if(d.success){e.className='ok';e.innerHTML='<pre style="margin:0;white-space:pre-wrap;font-size:13px">'+(d.data||'')+'</pre>';}else{e.className='ld';e.innerHTML='<pre style="margin:0;white-space:pre-wrap;font-size:13px">'+(d.data||'')+'\n\n'+sbuAdmin.i18n.downloadProgress+'</pre>';}refreshLog();}).catch(function(x){hideProgress();e.className='ld';e.textContent=sbuAdmin.i18n.downloadTimeout;});};
@@ -112,8 +111,7 @@
112111
'clear-log': function(){ window.sbuClearLog(); },
113112
'toggle-files': function(btn){ window.sbuToggle(btn.getAttribute('data-target'), btn); },
114113
'restore-all': function(btn){ window.sDlAll(btn.getAttribute('data-dir')); },
115-
'delete-backup': function(btn){ window.sDe(btn.getAttribute('data-dir')); },
116-
'download-file': function(btn){ window.sDl(btn.getAttribute('data-dir'), btn.getAttribute('data-file')); }
114+
'delete-backup': function(btn){ window.sDe(btn.getAttribute('data-dir')); }
117115
};
118116
var sbuRoot = document.querySelector('.wrap.sbu') || document;
119117
sbuRoot.addEventListener('click', function(e){

includes/class-sbu-plugin.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ function () {
131131
// Prune nach. Bei leerem Log oder retention=0 ist das ein No-Op
132132
// (cron_prune() bricht früh ab), also günstig.
133133
add_action( 'admin_init', array( $this->activity_logger, 'cron_prune' ) );
134-
foreach ( array( 'test', 'upload', 'list', 'download', 'download_all', 'delete', 'get_log', 'export_log', 'export_log_anon', 'clear_log', 'upload_status', 'load_libs', 'load_dirs', 'create_dir', 'save_settings', 'reset_settings', 'refresh_nonce', 'abort_upload', 'pause_upload', 'resume_upload', 'kick', 'dismiss_restore_banner', 'rotate_cron_key' ) as $a ) {
134+
foreach ( array( 'test', 'upload', 'list', 'download_all', 'delete', 'get_log', 'export_log', 'export_log_anon', 'clear_log', 'upload_status', 'load_libs', 'load_dirs', 'create_dir', 'save_settings', 'reset_settings', 'refresh_nonce', 'abort_upload', 'pause_upload', 'resume_upload', 'kick', 'dismiss_restore_banner', 'rotate_cron_key' ) as $a ) {
135135
add_action( 'wp_ajax_sbu_' . $a, array( $this, 'ajax_' . $a ) );
136136
}
137137
// External cron endpoint (no login required, key-protected)

includes/class-sbu-seafile-api.php

Lines changed: 2 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,9 @@
4040
*
4141
* The caller is responsible for:
4242
* - obtaining credentials and passing them in (see {@see self::get_token()}),
43-
* - deciding how long to cache the auth token,
43+
* - deciding how long to cache the auth token, and
4444
* - retrying on HTTP 401 after calling `get_token( ..., true )` for a fresh
45-
* token, and
46-
* - supplying a chunk size for {@see self::download_file()}.
45+
* token.
4746
*
4847
* @package seafile-updraft-backup-uploader
4948
*/
@@ -52,13 +51,6 @@
5251

5352
final class SBU_Seafile_API {
5453

55-
/**
56-
* Default chunk size (bytes) for {@see self::download_file()} when the
57-
* caller passes `$chunk_size <= 0`. Matches the plugin's own default of
58-
* 40 MB per chunk.
59-
*/
60-
const DEFAULT_DOWNLOAD_CHUNK = 20971520; // 20 MB — safe through Cloudflare Tunnel (~100 s connection limit); the plugin runs N of these in parallel via download_chunks_parallel().
61-
6254
/**
6355
* Obtain an auth token. Returns the cached transient unless `$force` is
6456
* true, in which case the transient is discarded and a fresh token
@@ -284,207 +276,6 @@ public static function get_download_link( $url, $token, $rid, $path ) {
284276
return $link;
285277
}
286278

287-
/**
288-
* Stream a file from Seafile to a local path, chunking with HTTP Range
289-
* requests when the remote is larger than the requested chunk size.
290-
* Link re-fetch on HTTP 403 (expired signed URL) is handled internally.
291-
*
292-
* @param string $url Seafile base URL.
293-
* @param string $token Auth token.
294-
* @param string $rid Repo ID.
295-
* @param string $path Remote file path.
296-
* @param string $dest Local destination path (will be overwritten).
297-
* @param int $chunk_size Bytes per Range request. Pass <= 0 to use
298-
* {@see self::DEFAULT_DOWNLOAD_CHUNK}.
299-
* @return true|\WP_Error
300-
*/
301-
public static function download_file( $url, $token, $rid, $path, $dest, $chunk_size = 0 ) {
302-
$link = self::get_download_link( $url, $token, $rid, $path );
303-
if ( is_wp_error( $link ) ) {
304-
return $link;
305-
}
306-
307-
if ( $chunk_size <= 0 ) {
308-
$chunk_size = self::DEFAULT_DOWNLOAD_CHUNK;
309-
}
310-
311-
// Get file size via HEAD
312-
$head = wp_remote_head(
313-
$link,
314-
array(
315-
'timeout' => SBU_TIMEOUT_API,
316-
'headers' => array( 'Authorization' => 'Token ' . $token ),
317-
)
318-
);
319-
$file_size = 0;
320-
if ( ! is_wp_error( $head ) ) {
321-
$file_size = (int) wp_remote_retrieve_header( $head, 'content-length' );
322-
}
323-
324-
// Small files or unknown size: download in one go
325-
if ( $file_size <= $chunk_size ) {
326-
$dl = wp_remote_get(
327-
$link,
328-
array(
329-
'timeout' => SBU_TIMEOUT_DOWNLOAD,
330-
'stream' => true,
331-
'filename' => $dest,
332-
'headers' => array(
333-
'Authorization' => 'Token ' . $token,
334-
'Connection' => 'close',
335-
),
336-
)
337-
);
338-
if ( is_wp_error( $dl ) ) {
339-
@unlink( $dest );
340-
return $dl;
341-
}
342-
$code = wp_remote_retrieve_response_code( $dl );
343-
// Refresh link on 403 and retry once
344-
if ( $code === 403 ) {
345-
@unlink( $dest );
346-
$link = self::get_download_link( $url, $token, $rid, $path );
347-
if ( is_wp_error( $link ) ) {
348-
return $link;
349-
}
350-
$dl = wp_remote_get(
351-
$link,
352-
array(
353-
'timeout' => SBU_TIMEOUT_DOWNLOAD,
354-
'stream' => true,
355-
'filename' => $dest,
356-
'headers' => array( 'Authorization' => 'Token ' . $token ),
357-
)
358-
);
359-
if ( is_wp_error( $dl ) ) {
360-
@unlink( $dest );
361-
return $dl;
362-
}
363-
$code = wp_remote_retrieve_response_code( $dl );
364-
}
365-
if ( $code !== 200 ) {
366-
@unlink( $dest );
367-
return new \WP_Error( 'http', 'HTTP ' . $code );
368-
}
369-
return true;
370-
}
371-
372-
// Large files: chunked download with Range headers
373-
@unlink( $dest );
374-
$offset = 0;
375-
$link_age = time();
376-
while ( $offset < $file_size ) {
377-
// Check for abort between chunks
378-
wp_cache_delete( 'sbu_abort_flag', 'transient' );
379-
wp_cache_delete( '_transient_sbu_abort_flag', 'options' );
380-
if ( get_transient( 'sbu_abort_flag' ) ) {
381-
@unlink( $dest );
382-
return new \WP_Error( 'aborted', 'Download abgebrochen' );
383-
}
384-
385-
// Refresh download link if older than 10 minutes
386-
if ( ( time() - $link_age ) > 600 ) {
387-
$link = self::get_download_link( $url, $token, $rid, $path );
388-
if ( is_wp_error( $link ) ) {
389-
@unlink( $dest );
390-
return $link;
391-
}
392-
$link_age = time();
393-
}
394-
395-
$end = min( $offset + $chunk_size - 1, $file_size - 1 );
396-
$tmp = $dest . '.part';
397-
398-
$dl = wp_remote_get(
399-
$link,
400-
array(
401-
'timeout' => SBU_TIMEOUT_DOWNLOAD,
402-
'stream' => true,
403-
'filename' => $tmp,
404-
'headers' => array(
405-
'Authorization' => 'Token ' . $token,
406-
'Connection' => 'close',
407-
'Range' => "bytes={$offset}-{$end}",
408-
),
409-
)
410-
);
411-
412-
if ( is_wp_error( $dl ) ) {
413-
@unlink( $tmp );
414-
@unlink( $dest );
415-
return $dl;
416-
}
417-
418-
$code = wp_remote_retrieve_response_code( $dl );
419-
420-
// Refresh link on 403 and retry this chunk
421-
if ( $code === 403 ) {
422-
@unlink( $tmp );
423-
$link = self::get_download_link( $url, $token, $rid, $path );
424-
if ( is_wp_error( $link ) ) {
425-
@unlink( $dest );
426-
return $link;
427-
}
428-
$link_age = time();
429-
$dl = wp_remote_get(
430-
$link,
431-
array(
432-
'timeout' => SBU_TIMEOUT_DOWNLOAD,
433-
'stream' => true,
434-
'filename' => $tmp,
435-
'headers' => array(
436-
'Authorization' => 'Token ' . $token,
437-
'Range' => "bytes={$offset}-{$end}",
438-
),
439-
)
440-
);
441-
if ( is_wp_error( $dl ) ) {
442-
@unlink( $tmp );
443-
@unlink( $dest );
444-
return $dl;
445-
}
446-
$code = wp_remote_retrieve_response_code( $dl );
447-
}
448-
449-
if ( $code !== 206 && $code !== 200 ) {
450-
@unlink( $tmp );
451-
@unlink( $dest );
452-
return new \WP_Error( 'http', "HTTP {$code} bei Offset {$offset}" );
453-
}
454-
455-
// Append chunk to destination
456-
$fh = fopen( $dest, $offset === 0 ? 'wb' : 'ab' );
457-
if ( ! $fh ) {
458-
@unlink( $tmp );
459-
return new \WP_Error( 'io', 'Cannot open dest' );
460-
}
461-
$th = fopen( $tmp, 'rb' );
462-
if ( $th ) {
463-
while ( ! feof( $th ) ) {
464-
fwrite( $fh, fread( $th, 65536 ) );
465-
}
466-
fclose( $th );
467-
}
468-
fclose( $fh );
469-
@unlink( $tmp );
470-
471-
$written = filesize( $dest );
472-
$offset = $written;
473-
474-
// If server returned 200 instead of 206, it sent the whole file
475-
if ( $code === 200 ) {
476-
break;
477-
}
478-
}
479-
480-
// Verify final size
481-
if ( file_exists( $dest ) && filesize( $dest ) >= $file_size ) {
482-
return true;
483-
}
484-
$actual = file_exists( $dest ) ? filesize( $dest ) : 0;
485-
return new \WP_Error( 'incomplete', "Download unvollständig: {$actual}/{$file_size} Bytes" );
486-
}
487-
488279
/**
489280
* Download the whole file in a single streamed GET — no Range header.
490281
*

0 commit comments

Comments
 (0)