@@ -2111,4 +2111,116 @@ export class GitHubRepository extends Disposable {
21112111 }
21122112 return CheckState . Success ;
21132113 }
2114+
2115+ /**
2116+ * Upload a file to GitHub via the mobile upload policy API. Returns a markdown
2117+ * snippet appropriate for embedding in an issue/PR comment.
2118+ */
2119+ public async uploadFile ( uri : vscode . Uri , fileName : string ) : Promise < string > {
2120+ // Guard against very large files: check size before reading the bytes into memory.
2121+ let fileSize : number | undefined ;
2122+ try {
2123+ const stat = await vscode . workspace . fs . stat ( uri ) ;
2124+ fileSize = stat . size ;
2125+ } catch {
2126+ // Fall through; readFile will surface a more specific error if needed.
2127+ }
2128+ if ( fileSize !== undefined && fileSize > MAX_UPLOAD_SIZE_BYTES ) {
2129+ throw new Error ( `File "${ fileName } " is too large to upload (${ Math . round ( fileSize / ( 1024 * 1024 ) ) } MB). The maximum allowed size is ${ MAX_UPLOAD_SIZE_BYTES / ( 1024 * 1024 ) } MB.` ) ;
2130+ }
2131+
2132+ const fileBytes = await vscode . workspace . fs . readFile ( uri ) ;
2133+ if ( fileBytes . byteLength > MAX_UPLOAD_SIZE_BYTES ) {
2134+ throw new Error ( `File "${ fileName } " is too large to upload (${ Math . round ( fileBytes . byteLength / ( 1024 * 1024 ) ) } MB). The maximum allowed size is ${ MAX_UPLOAD_SIZE_BYTES / ( 1024 * 1024 ) } MB.` ) ;
2135+ }
2136+ const contentType = guessContentType ( fileName ) ;
2137+
2138+ const { octokit } = await this . ensure ( ) ;
2139+ const metadata = await this . getMetadata ( ) ;
2140+ const repositoryId = metadata . id ;
2141+
2142+ // Step 1: Get upload policy
2143+ const policyResponse = await octokit . api . request ( 'POST /mobile/upload/policy' , {
2144+ name : fileName ,
2145+ size : fileBytes . byteLength ,
2146+ content_type : contentType ,
2147+ repository_id : repositoryId ,
2148+ headers : { accept : 'application/json' } ,
2149+ } ) ;
2150+ const policy = policyResponse . data as {
2151+ upload_url : string ;
2152+ form : Record < string , string > ;
2153+ asset : { id : number ; name : string ; href : string } ;
2154+ asset_upload_url : string ;
2155+ } ;
2156+
2157+ // Step 2: Upload bytes to the storage location returned by the policy.
2158+ // Pass the Uint8Array directly to Blob to avoid an extra full-size copy.
2159+ const formData = new FormData ( ) ;
2160+ for ( const [ key , value ] of Object . entries ( policy . form ) ) {
2161+ formData . append ( key , value ) ;
2162+ }
2163+ // The DOM Blob types require Uint8Array<ArrayBuffer>, but vscode.workspace.fs.readFile
2164+ // returns Uint8Array<ArrayBufferLike>. The runtime accepts it, so cast via unknown to avoid a copy.
2165+ formData . append ( 'file' , new Blob ( [ fileBytes as unknown as BlobPart ] , { type : contentType } ) , policy . asset . name ) ;
2166+ const s3Response = await fetch ( policy . upload_url , { method : 'POST' , body : formData } ) ;
2167+ if ( s3Response . status !== 204 && s3Response . status !== 201 && s3Response . status !== 200 ) {
2168+ throw new Error ( `Storage upload failed with status ${ s3Response . status } ` ) ;
2169+ }
2170+
2171+ // Step 3: Confirm the upload with GitHub
2172+ await octokit . api . request ( `PUT ${ policy . asset_upload_url } ` , {
2173+ headers : { accept : 'application/json' } ,
2174+ } ) ;
2175+
2176+ const url = policy . asset . href ;
2177+ const safeName = escapeMarkdownLinkText ( fileName ) ;
2178+ if ( contentType . startsWith ( 'image/' ) ) {
2179+ return `` ;
2180+ }
2181+ if ( contentType . startsWith ( 'video/' ) ) {
2182+ return url ;
2183+ }
2184+ return `[${ safeName } ](${ url } )` ;
2185+ }
2186+ }
2187+
2188+ const MAX_UPLOAD_SIZE_BYTES = 25 * 1024 * 1024 ; // 25 MB
2189+
2190+ /**
2191+ * Escape characters that would break a markdown link's text segment (`[text](url)`).
2192+ * Filenames may legally contain `[`, `]`, `\`, etc., which can corrupt the rendered link.
2193+ */
2194+ function escapeMarkdownLinkText ( text : string ) : string {
2195+ return text . replace ( / ( [ \\ \[ \] ` ] ) / g, '\\$1' ) ;
2196+ }
2197+
2198+ function guessContentType ( fileName : string ) : string {
2199+ const lastDot = fileName . lastIndexOf ( '.' ) ;
2200+ const ext = lastDot >= 0 ? fileName . substring ( lastDot ) . toLowerCase ( ) : '' ;
2201+ switch ( ext ) {
2202+ case '.png' : return 'image/png' ;
2203+ case '.jpg' :
2204+ case '.jpeg' : return 'image/jpeg' ;
2205+ case '.gif' : return 'image/gif' ;
2206+ case '.webp' : return 'image/webp' ;
2207+ case '.svg' : return 'image/svg+xml' ;
2208+ case '.bmp' : return 'image/bmp' ;
2209+ case '.heic' : return 'image/heic' ;
2210+ case '.mp4' : return 'video/mp4' ;
2211+ case '.mov' : return 'video/quicktime' ;
2212+ case '.webm' : return 'video/webm' ;
2213+ case '.pdf' : return 'application/pdf' ;
2214+ case '.zip' : return 'application/zip' ;
2215+ case '.gz' : return 'application/gzip' ;
2216+ case '.tar' : return 'application/x-tar' ;
2217+ case '.txt' : return 'text/plain' ;
2218+ case '.md' : return 'text/markdown' ;
2219+ case '.json' : return 'application/json' ;
2220+ case '.log' : return 'text/plain' ;
2221+ case '.docx' : return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ;
2222+ case '.xlsx' : return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ;
2223+ case '.pptx' : return 'application/vnd.openxmlformats-officedocument.presentationml.presentation' ;
2224+ default : return 'application/octet-stream' ;
2225+ }
21142226}
0 commit comments