Skip to content

Commit 58a3bba

Browse files
authored
fix(s3): handle content types more gracefully (#3062)
## Problem * Uploading files from the file editor overwrites the existing content type * Editing files without extension on S3 changes the Metadata of the object #3055 * Non-text files without a file extension throw an error ## Solution * Shift the ContentType parameter up so callers have to be explicit * Treat files with no mime type as binary files I also refactored fileViewerManager.ts a bit so it could be tested with significantly less mocks
1 parent 80464a4 commit 58a3bba

File tree

10 files changed

+251
-244
lines changed

10 files changed

+251
-244
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "S3: saving files from the editor overwrites existing content types"
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Bug Fix",
3+
"description": "S3: file editor fails on binary files with no file extension"
4+
}

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/s3/commands/uploadFile.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { S3 } from 'aws-sdk'
76
import * as path from 'path'
8-
import { statSync } from 'fs'
7+
import * as mime from 'mime-types'
98
import * as vscode from 'vscode'
10-
9+
import { statSync } from 'fs'
10+
import { S3 } from 'aws-sdk'
1111
import { getLogger } from '../../shared/logger'
1212
import { S3Node } from '../explorer/s3Nodes'
1313
import { Commands } from '../../shared/vscode/commands'
@@ -365,6 +365,7 @@ async function uploadWithProgress(
365365
key: request.key,
366366
content: request.fileLocation,
367367
progressListener,
368+
contentType: mime.contentType(path.extname(request.fileLocation.fsPath)) || undefined,
368369
})
369370

370371
progressListener(0)

src/s3/explorer/s3BucketNode.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@
55

66
import * as vscode from 'vscode'
77
import { ChildNodePage } from '../../awsexplorer/childNodeLoader'
8-
import {
9-
Bucket,
10-
CreateFolderRequest,
11-
CreateFolderResponse,
12-
S3Client,
13-
UploadFileRequest,
14-
} from '../../shared/clients/s3Client'
8+
import { Bucket, CreateFolderRequest, CreateFolderResponse, S3Client } from '../../shared/clients/s3Client'
159

1610
import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode'
1711
import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
@@ -91,14 +85,6 @@ export class S3BucketNode extends AWSTreeNodeBase implements AWSResourceNode, Lo
9185
return this.s3.createFolder(request)
9286
}
9387

94-
/**
95-
* See {@link S3Client.uploadFile}.
96-
*/
97-
public async uploadFile(request: UploadFileRequest): Promise<void> {
98-
const managedUpload = await this.s3.uploadFile(request)
99-
await managedUpload.promise()
100-
}
101-
10288
/**
10389
* See {@link S3Client.deleteBucket}.
10490
*/

src/s3/explorer/s3FolderNode.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,7 @@
44
*/
55

66
import * as vscode from 'vscode'
7-
import {
8-
Bucket,
9-
CreateFolderRequest,
10-
CreateFolderResponse,
11-
Folder,
12-
S3Client,
13-
UploadFileRequest,
14-
} from '../../shared/clients/s3Client'
7+
import { Bucket, CreateFolderRequest, CreateFolderResponse, Folder, S3Client } from '../../shared/clients/s3Client'
158
import { AWSResourceNode } from '../../shared/treeview/nodes/awsResourceNode'
169
import { AWSTreeNodeBase } from '../../shared/treeview/nodes/awsTreeNodeBase'
1710
import { LoadMoreNode } from '../../shared/treeview/nodes/loadMoreNode'
@@ -90,14 +83,6 @@ export class S3FolderNode extends AWSTreeNodeBase implements AWSResourceNode, Lo
9083
return this.s3.createFolder(request)
9184
}
9285

93-
/**
94-
* See {@link S3Client.uploadFile}.
95-
*/
96-
public async uploadFile(request: UploadFileRequest): Promise<void> {
97-
const managedUpload = await this.s3.uploadFile(request)
98-
await managedUpload.promise()
99-
}
100-
10186
public get arn(): string {
10287
return this.folder.arn
10388
}

src/s3/fileViewerManager.ts

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { s3FileViewerHelpUrl } from '../shared/constants'
1616
import { FileProvider, VirualFileSystem } from '../shared/virtualFilesystem'
1717
import { PromptSettings } from '../shared/settings'
1818
import { telemetry } from '../shared/telemetry/telemetry'
19+
import { ToolkitError } from '../shared/errors'
1920

2021
export const S3_EDIT_SCHEME = 's3'
2122
export const S3_READ_SCHEME = 's3-readonly'
@@ -54,8 +55,9 @@ export class S3FileProvider implements FileProvider {
5455
const stats = await this.client.headObject({ bucketName: bucket.name, key })
5556

5657
this.updateETag(stats.ETag)
57-
this._file.sizeBytes = stats.ContentLength
58+
this._file.sizeBytes = stats.ContentLength ?? this._file.sizeBytes
5859
this._file.lastModified = stats.LastModified
60+
this._file.ContentType = stats.ContentType
5961
}
6062

6163
public async read(): Promise<Uint8Array> {
@@ -88,12 +90,13 @@ export class S3FileProvider implements FileProvider {
8890
return telemetry.s3_uploadObject.run(async span => {
8991
span.record({ component: 'viewer' })
9092

93+
const mimeType = mime.contentType(path.extname(this._file.name)) || undefined
9194
const result = await this.client
9295
.uploadFile({
9396
content,
9497
key: this._file.key,
9598
bucketName: this._file.bucket.name,
96-
contentType: mime.contentType(path.extname(this._file.name)) || undefined,
99+
contentType: this._file.ContentType ?? mimeType,
97100
})
98101
.then(u => u.promise())
99102

@@ -123,8 +126,7 @@ export class S3FileViewerManager {
123126
private readonly fs: VirualFileSystem,
124127
private readonly window: typeof vscode.window = vscode.window,
125128
private readonly settings = PromptSettings.instance,
126-
private readonly commands: typeof vscode.commands = vscode.commands,
127-
private readonly workspace: typeof vscode.workspace = vscode.workspace
129+
private readonly schemes = { read: S3_READ_SCHEME, edit: S3_EDIT_SCHEME }
128130
) {
129131
this.disposables.push(this.registerTabCleanup())
130132
}
@@ -141,7 +143,7 @@ export class S3FileViewerManager {
141143
}
142144

143145
private registerTabCleanup(): vscode.Disposable {
144-
return this.workspace.onDidCloseTextDocument(async doc => {
146+
return vscode.workspace.onDidCloseTextDocument(async doc => {
145147
const key = this.fs.uriToKey(doc.uri)
146148
await this.activeTabs[key]?.dispose()
147149
})
@@ -155,26 +157,29 @@ export class S3FileViewerManager {
155157
options?: vscode.TextDocumentShowOptions
156158
): Promise<vscode.TextEditor | undefined> {
157159
const fsPath = fileUri.fsPath
158-
159160
await this.activeTabs[this.fs.uriToKey(fileUri)]?.dispose()
160161

161-
// Defer to `vscode.open` for non-text files
162-
const contentType = mime.contentType(path.extname(fsPath))
163-
if (contentType && mime.charset(contentType) != 'UTF-8') {
164-
await this.commands.executeCommand('vscode.open', fileUri)
165-
return this.window.visibleTextEditors.find(
166-
e => this.fs.uriToKey(e.document.uri) === this.fs.uriToKey(fileUri)
167-
)
168-
}
162+
try {
163+
// Defer to `vscode.open` for non-text files
164+
const contentType = mime.contentType(path.extname(fsPath))
165+
if (!contentType || mime.charset(contentType) !== 'UTF-8') {
166+
await vscode.commands.executeCommand('vscode.open', fileUri)
167+
return this.window.visibleTextEditors.find(
168+
e => this.fs.uriToKey(e.document.uri) === this.fs.uriToKey(fileUri)
169+
)
170+
}
169171

170-
const document = await this.workspace.openTextDocument(fileUri)
171-
return await this.window.showTextDocument(document, options)
172+
const document = await vscode.workspace.openTextDocument(fileUri)
173+
return await this.window.showTextDocument(document, options)
174+
} catch (err) {
175+
throw ToolkitError.chain(err, 'Failed to open document', { code: 'FailedToOpen' })
176+
}
172177
}
173178

174179
private async closeEditor(editor: vscode.TextEditor | undefined): Promise<void> {
175180
if (editor && !editor.document.isClosed) {
176181
await this.window.showTextDocument(editor.document, { preserveFocus: false })
177-
await this.commands.executeCommand('workbench.action.closeActiveEditor')
182+
await vscode.commands.executeCommand('workbench.action.closeActiveEditor')
178183
}
179184
}
180185

@@ -192,7 +197,7 @@ export class S3FileViewerManager {
192197
await this.window.showTextDocument(activeTab.editor.document)
193198
} else {
194199
getLogger().verbose(`S3FileViewer: Reopening non-text document`)
195-
await this.commands.executeCommand('vscode.open', uri)
200+
await vscode.commands.executeCommand('vscode.open', uri)
196201
}
197202

198203
return activeTab
@@ -209,8 +214,8 @@ export class S3FileViewerManager {
209214
const contentType = mime.contentType(path.extname(file.name))
210215
const isTextDocument = contentType && mime.charset(contentType) == 'UTF-8'
211216

212-
const uri = S3FileViewerManager.fileToUri(file, TabMode.Read)
213-
if (await this.tryFocusTab(uri, uri.with({ scheme: S3_EDIT_SCHEME }))) {
217+
const uri = this.fileToUri(file, TabMode.Read)
218+
if (await this.tryFocusTab(uri, uri.with({ scheme: this.schemes.edit }))) {
214219
return
215220
}
216221

@@ -224,13 +229,13 @@ export class S3FileViewerManager {
224229

225230
/**
226231
* Opens the tab in edit mode with the use of an S3Tab, or shifts focus to an edit tab if any.
227-
* Exiting read-only tabs are closed as they cannot be converted to edit tabs.
232+
* Existing read-only tabs are closed as they cannot be converted to edit tabs.
228233
*
229234
* @param uriOrFile to be opened
230235
*/
231236
public async openInEditMode(uriOrFile: vscode.Uri | S3File): Promise<void> {
232-
const uri = uriOrFile instanceof vscode.Uri ? uriOrFile : S3FileViewerManager.fileToUri(uriOrFile, TabMode.Edit)
233-
const activeTab = await this.tryFocusTab(uri, uri.with({ scheme: S3_READ_SCHEME }))
237+
const uri = uriOrFile instanceof vscode.Uri ? uriOrFile : this.fileToUri(uriOrFile, TabMode.Edit)
238+
const activeTab = await this.tryFocusTab(uri, uri.with({ scheme: this.schemes.read }))
234239
const file = activeTab?.file ?? uriOrFile
235240

236241
if (activeTab?.mode === TabMode.Edit) {
@@ -254,7 +259,7 @@ export class S3FileViewerManager {
254259
this.fs.registerProvider(uri, provider),
255260
provider.onDidChange(() => {
256261
// TODO: find the correct node instead of refreshing it all
257-
this.commands.executeCommand('aws.refreshAwsExplorer', true)
262+
vscode.commands.executeCommand('aws.refreshAwsExplorer', true)
258263
})
259264
)
260265
}
@@ -267,7 +272,7 @@ export class S3FileViewerManager {
267272
throw new CancellationError('user')
268273
}
269274

270-
const uri = S3FileViewerManager.fileToUri(file, mode)
275+
const uri = this.fileToUri(file, mode)
271276
const key = this.fs.uriToKey(uri)
272277
const provider = (this.providers[key] ??= this.registerProvider(file, uri))
273278
const editor = await this.openEditor(uri, { preview: mode === TabMode.Read })
@@ -353,8 +358,8 @@ export class S3FileViewerManager {
353358
})
354359
}
355360

356-
private static fileToUri(file: S3File, mode: TabMode): vscode.Uri {
357-
const scheme = mode === TabMode.Read ? S3_READ_SCHEME : S3_EDIT_SCHEME
361+
private fileToUri(file: S3File, mode: TabMode): vscode.Uri {
362+
const scheme = mode === TabMode.Read ? this.schemes.read : this.schemes.edit
358363

359364
return vscode.Uri.parse(`${scheme}:`, true).with({
360365
path: ['', file.bucket.region, file.bucket.name, file.key].join('/'),

0 commit comments

Comments
 (0)