ソースを参照

fix(frontend): keep the given file name on mobile downloads

FileManager typed every download text/plain. Android's MediaStore appends
the MIME type's extension whenever the name's own extension maps elsewhere,
so a subscriber saving a WireGuard config got peer.conf.txt, which the
WireGuard app refuses; .json, .yaml and .log downloads were renamed the same
way. Desktop browsers honour the download name, which is why only phones
saw it. application/octet-stream carries no extension of its own, so the
name the panel chose is kept.
MHSanaei 8 時間 前
親
コミット
99047c0a63
2 ファイル変更、38 行追加、1 行削除
  1. 35 0
      frontend/src/test/file-download.test.tsx
  2. 3 1
      frontend/src/utils/index.ts

+ 35 - 0
frontend/src/test/file-download.test.tsx

@@ -0,0 +1,35 @@
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { FileManager } from '@/utils';
+
+const { createObjectURL, revokeObjectURL } = URL;
+
+afterEach(() => {
+  URL.createObjectURL = createObjectURL;
+  URL.revokeObjectURL = revokeObjectURL;
+  vi.restoreAllMocks();
+});
+
+describe('FileManager.downloadTextFile', () => {
+  // Android's MediaStore appends the blob type's own extension when the name's
+  // extension maps elsewhere, so a text/plain peer.conf lands as peer.conf.txt.
+  it('hands the browser an untyped blob so a mobile save keeps the given name', () => {
+    const blobs: Blob[] = [];
+    URL.createObjectURL = vi.fn((blob: Blob) => {
+      blobs.push(blob);
+      return 'blob:download';
+    });
+    URL.revokeObjectURL = vi.fn();
+    let savedAs = '';
+    vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(function (
+      this: HTMLAnchorElement,
+    ) {
+      savedAs = this.download;
+    });
+
+    FileManager.downloadTextFile('[Interface]\n', 'alice.conf');
+
+    expect(savedAs).toBe('alice.conf');
+    expect(blobs.map((blob) => blob.type)).toEqual(['application/octet-stream']);
+  });
+});

+ 3 - 1
frontend/src/utils/index.ts

@@ -967,10 +967,12 @@ export class LanguageManager {
 }
 
 export class FileManager {
+  // Typed text/plain, Android's MediaStore saves peer.conf as peer.conf.txt;
+  // octet-stream carries no extension of its own, so the given name is kept.
   static downloadTextFile(
     content: BlobPart,
     filename: string = 'file.txt',
-    options: BlobPropertyBag = { type: 'text/plain' },
+    options: BlobPropertyBag = { type: 'application/octet-stream' },
   ): void {
     const link = window.document.createElement('a');
     link.download = filename;