Skip to content

Commit a8405b4

Browse files
authored
S3: remember last-used "Download As" directory #2971
Problem: When downloading S3 files, the "Save As" dialog always defaults to the Downloads directory. Solution: Store the last-used download path and use it for next save prompt. ref #2855
1 parent f16eab7 commit a8405b4

File tree

4 files changed

+48
-5
lines changed

4 files changed

+48
-5
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "Feature",
3+
"description": "S3: \"Download As\" action defaults to the last-used download path."
4+
}

src/s3/commands/downloadFileAs.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as path from 'path'
77
import * as vscode from 'vscode'
88

9-
import { downloadsDir } from '../../shared/filesystemUtilities'
9+
import { getDefaultDownloadPath, setDefaultDownloadPath } from '../../shared/filesystemUtilities'
1010
import { Window } from '../../shared/vscode/window'
1111
import { S3FileNode } from '../explorer/s3FileNode'
1212
import { readablePath } from '../util'
@@ -119,10 +119,13 @@ export async function downloadFileAsCommand(
119119
const sourcePath = readablePath(node)
120120

121121
await telemetry.s3_downloadObject.run(async () => {
122-
const saveLocation = await promptForSaveLocation(file.name, window)
122+
const downloadPath = getDefaultDownloadPath()
123+
124+
const saveLocation = await promptForSaveLocation(file.name, window, downloadPath)
123125
if (!saveLocation) {
124126
throw new CancellationError('user')
125127
}
128+
setDefaultDownloadPath(saveLocation.fsPath)
126129

127130
showOutputMessage(`Downloading "${sourcePath}" to: ${saveLocation}`, outputChannel)
128131

@@ -140,7 +143,11 @@ export async function downloadFileAsCommand(
140143
})
141144
}
142145

143-
async function promptForSaveLocation(fileName: string, window: Window): Promise<vscode.Uri | undefined> {
146+
async function promptForSaveLocation(
147+
fileName: string,
148+
window: Window,
149+
saveLocation: string
150+
): Promise<vscode.Uri | undefined> {
144151
const extension = path.extname(fileName)
145152

146153
// Insertion order matters, as it determines the ordering in the filters dropdown
@@ -149,7 +156,7 @@ async function promptForSaveLocation(fileName: string, window: Window): Promise<
149156
? { [`*${extension}`]: [extension.slice(1)], 'All Files': ['*'] }
150157
: { 'All Files': ['*'] }
151158

152-
const downloadPath = path.join(downloadsDir(), fileName)
159+
const downloadPath = path.join(saveLocation, fileName)
153160
return window.showSaveDialog({
154161
defaultUri: vscode.Uri.file(downloadPath),
155162
saveLabel: localize('AWS.s3.downloadFile.saveButton', 'Download'),

src/shared/filesystemUtilities.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as path from 'path'
1212
import * as vscode from 'vscode'
1313
import { getLogger } from './logger'
1414
import * as pathutils from './utilities/pathUtils'
15+
import globals from '../shared/extensionGlobals'
1516

1617
const DEFAULT_ENCODING: BufferEncoding = 'utf8'
1718

@@ -224,3 +225,29 @@ export async function cloud9Findfile(dir: string, fileName: string): Promise<vsc
224225
}
225226
return []
226227
}
228+
/**
229+
* @returns A string path to the last locally stored download location. If none, returns the users 'Downloads' directory path.
230+
*/
231+
export function getDefaultDownloadPath(): string {
232+
const lastUsedPath = globals.context.globalState.get('aws.downloadPath')
233+
if (lastUsedPath) {
234+
if (typeof lastUsedPath === 'string') {
235+
return lastUsedPath
236+
}
237+
getLogger().error('Expected "aws.downloadPath" to be string, got %O', typeof lastUsedPath)
238+
}
239+
return downloadsDir()
240+
}
241+
242+
export async function setDefaultDownloadPath(downloadPath: string) {
243+
try {
244+
const savePath = await stat(downloadPath)
245+
if (savePath.isDirectory()) {
246+
globals.context.globalState.update('aws.downloadPath', downloadPath)
247+
} else {
248+
globals.context.globalState.update('aws.downloadPath', path.dirname(downloadPath))
249+
}
250+
} catch (err) {
251+
getLogger().error('Error while setting "aws.downloadPath"', err as Error)
252+
}
253+
}

src/test/s3/commands/downloadFileAs.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { MockOutputChannel } from '../../mockOutputChannel'
1616
import { FakeWindow } from '../../shared/vscode/fakeWindow'
1717
import { anything, mock, instance, when, verify } from '../../utilities/mockito'
1818
import { makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities'
19+
import globals from '../../../shared/extensionGlobals'
20+
import { assertEqualPaths } from '../../testUtil'
1921

2022
describe('downloadFileAsCommand', function () {
2123
const bucketName = 'bucket-name'
@@ -52,12 +54,15 @@ describe('downloadFileAsCommand', function () {
5254
it('prompts for save location, downloads file with progress, and shows output channel', async function () {
5355
const window = new FakeWindow({ dialog: { saveSelection: saveLocation } })
5456
const outputChannel = new MockOutputChannel()
57+
globals.context.globalState.update('aws.downloadPath', temp)
5558

5659
when(s3.downloadFileStream(anything(), anything())).thenResolve(bufferToStream(Buffer.alloc(16)))
5760

5861
await downloadFileAsCommand(node, window, outputChannel)
5962

60-
assert.ok(window.dialog.saveOptions?.defaultUri?.path?.endsWith(fileName))
63+
assert.ok(window.dialog.saveOptions?.defaultUri?.fsPath)
64+
assertEqualPaths(window.dialog.saveOptions.defaultUri.fsPath, path.join(temp, 'file.jpg'))
65+
6166
assert.strictEqual(window.dialog.saveOptions?.saveLabel, 'Download')
6267
assert.deepStrictEqual(window.dialog.saveOptions?.filters, { 'All Files': ['*'], '*.jpg': ['jpg'] })
6368

0 commit comments

Comments
 (0)