33 * Licensed under the MIT License. See License.txt in the project root for license information.
44 *--------------------------------------------------------------------------------------------*/
55
6+ import * as buffer from 'buffer' ;
67import * as path from 'path' ;
78import * as vscode from 'vscode' ;
89import { GitHubRepository } from './githubRepository' ;
@@ -15,6 +16,61 @@ export interface FileUploadPlaceholder {
1516 placeholder : string ;
1617}
1718
19+ export interface PendingFileUpload {
20+ name : string ;
21+ placeholder : string ;
22+ getBytes ( ) : Thenable < Uint8Array > ;
23+ }
24+
25+ /**
26+ * Decode a base64 string to a {@linkcode Uint8Array}.
27+ */
28+ export function decodeBase64 ( input : string ) : Uint8Array {
29+ return buffer . Buffer . from ( input , 'base64' ) ;
30+ }
31+
32+ /**
33+ * Guess a file extension (including the dot) for a given MIME type, falling back
34+ * to an empty string when no good guess is available.
35+ */
36+ export function guessExtensionFromMime ( mimeType : string ) : string {
37+ const lower = mimeType . toLowerCase ( ) ;
38+ switch ( lower ) {
39+ case 'image/png' : return '.png' ;
40+ case 'image/jpeg' : return '.jpg' ;
41+ case 'image/gif' : return '.gif' ;
42+ case 'image/webp' : return '.webp' ;
43+ case 'image/svg+xml' : return '.svg' ;
44+ case 'image/bmp' : return '.bmp' ;
45+ case 'image/heic' : return '.heic' ;
46+ case 'video/mp4' : return '.mp4' ;
47+ case 'video/quicktime' : return '.mov' ;
48+ case 'video/webm' : return '.webm' ;
49+ case 'application/pdf' : return '.pdf' ;
50+ case 'application/zip' : return '.zip' ;
51+ case 'application/json' : return '.json' ;
52+ case 'text/plain' : return '.txt' ;
53+ case 'text/markdown' : return '.md' ;
54+ default : return '' ;
55+ }
56+ }
57+
58+ /**
59+ * Compute placeholder strings for the given file names, deduplicating
60+ * by name with `(2)`, `(3)` suffixes.
61+ */
62+ export function placeholdersForNames ( names : readonly string [ ] ) : { name : string ; placeholder : string } [ ] {
63+ const used = new Map < string , number > ( ) ;
64+ return names . map ( name => {
65+ const count = used . get ( name ) ?? 0 ;
66+ used . set ( name , count + 1 ) ;
67+ const placeholder = count === 0
68+ ? `<!-- Uploading ${ name } -->`
69+ : `<!-- Uploading ${ name } (${ count + 1 } ) -->` ;
70+ return { name, placeholder } ;
71+ } ) ;
72+ }
73+
1874/**
1975 * Prompt the user for files to upload and compute the placeholder text that
2076 * should be inserted into a comment textarea while the uploads run.
@@ -32,16 +88,9 @@ export async function pickFilesForUpload(): Promise<FileUploadPlaceholder[] | un
3288 return undefined ;
3389 }
3490
35- const used = new Map < string , number > ( ) ;
36- return fileUris . map ( uri => {
37- const baseName = path . basename ( uri . fsPath ) ;
38- const count = used . get ( baseName ) ?? 0 ;
39- used . set ( baseName , count + 1 ) ;
40- const placeholder = count === 0
41- ? `<!-- Uploading ${ baseName } -->`
42- : `<!-- Uploading ${ baseName } (${ count + 1 } ) -->` ;
43- return { uri, name : baseName , placeholder } ;
44- } ) ;
91+ const names = fileUris . map ( uri => path . basename ( uri . fsPath ) ) ;
92+ const placeholders = placeholdersForNames ( names ) ;
93+ return fileUris . map ( ( uri , i ) => ( { uri, name : placeholders [ i ] . name , placeholder : placeholders [ i ] . placeholder } ) ) ;
4594}
4695
4796/**
@@ -56,7 +105,30 @@ const MAX_CONCURRENT_UPLOADS = 3;
56105 */
57106export function runFileUploads (
58107 githubRepository : GitHubRepository ,
59- uploads : FileUploadPlaceholder [ ] ,
108+ uploads : readonly FileUploadPlaceholder [ ] ,
109+ logId : string ,
110+ onComplete : ( placeholder : string , name : string , markdown : string ) => void | Promise < void > ,
111+ onError : ( placeholder : string , name : string , error : string ) => void | Promise < void > ,
112+ ) : void {
113+ runPendingUploads (
114+ githubRepository ,
115+ uploads . map ( u => ( {
116+ name : u . name ,
117+ placeholder : u . placeholder ,
118+ getBytes : ( ) => vscode . workspace . fs . readFile ( u . uri ) ,
119+ } ) ) ,
120+ logId ,
121+ onComplete ,
122+ onError ,
123+ ) ;
124+ }
125+
126+ /**
127+ * Run uploads in parallel, fetching the bytes lazily via {@linkcode PendingFileUpload.getBytes}.
128+ */
129+ export function runPendingUploads (
130+ githubRepository : GitHubRepository ,
131+ uploads : readonly PendingFileUpload [ ] ,
60132 logId : string ,
61133 onComplete : ( placeholder : string , name : string , markdown : string ) => void | Promise < void > ,
62134 onError : ( placeholder : string , name : string , error : string ) => void | Promise < void > ,
@@ -66,13 +138,15 @@ export function runFileUploads(
66138 const runOne = async ( ) : Promise < void > => {
67139 while ( next < uploads . length ) {
68140 const u = uploads [ next ++ ] ;
69- try {
70- const markdown = await githubRepository . uploadFile ( u . uri , u . name ) ;
71- await onComplete ( u . placeholder , u . name , markdown ) ;
72- } catch ( err ) {
141+ ( async ( ) => {
142+ const bytes = await u . getBytes ( ) ;
143+ return githubRepository . uploadFileBytes ( bytes , u . name ) ;
144+ } ) ( ) . then ( markdown => {
145+ return onComplete ( u . placeholder , u . name , markdown ) ;
146+ } ) . catch ( err => {
73147 Logger . error ( `Failed to upload file ${ u . name } : ${ formatError ( err ) } ` , logId ) ;
74- await onError ( u . placeholder , u . name , formatError ( err ) ) ;
75- }
148+ return onError ( u . placeholder , u . name , formatError ( err ) ) ;
149+ } ) ;
76150 }
77151 } ;
78152
0 commit comments