Skip to content

Commit 38601dc

Browse files
Scott DoverScott Dover
authored andcommitted
feat: allow update/rename of files and folders
Signed-off-by: Scott Dover <[email protected]>
1 parent 1191019 commit 38601dc

File tree

3 files changed

+114
-109
lines changed

3 files changed

+114
-109
lines changed

client/src/connection/rest/RestSASServerAdapter.ts

Lines changed: 109 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { FileType, Uri } from "vscode";
22

3+
import { AxiosResponse } from "axios";
4+
35
import { getSession } from "..";
46
import {
57
FOLDER_TYPES,
@@ -15,30 +17,34 @@ import {
1517
} from "../../components/ContentNavigator/types";
1618
import {
1719
createStaticFolder,
18-
isItemInRecycleBin,
1920
isReference,
2021
} from "../../components/ContentNavigator/utils";
2122
import { appendSessionLogFn } from "../../components/logViewer";
2223
import { FileProperties, FileSystemApi } from "./api/compute";
2324
import { getApiConfig } from "./common";
2425
import {
2526
getLink,
26-
getPermission,
2727
getResourceId,
2828
getResourceIdFromItem,
2929
getSasServerUri,
3030
getTypeName,
3131
resourceType,
3232
} from "./util";
3333

34+
const SAS_SERVER_HOME_DIRECTORY = "SAS_SERVER_HOME_DIRECTORY";
35+
3436
class RestSASServerAdapter implements ContentAdapter {
3537
protected baseUrl: string;
3638
protected fileSystemApi: ReturnType<typeof FileSystemApi>;
3739
protected sessionId: string;
3840
private rootFolders: RootFolderMap;
41+
private fileMetadataMap: {
42+
[id: string]: { etag: string; lastModified?: string; contentType?: string };
43+
};
3944

4045
public constructor() {
4146
this.rootFolders = {};
47+
this.fileMetadataMap = {};
4248
}
4349

4450
public async connect(): Promise<void> {
@@ -82,16 +88,11 @@ class RestSASServerAdapter implements ContentAdapter {
8288
): Promise<ContentItem | undefined> {
8389
const response = await this.fileSystemApi.createFileOrDirectory({
8490
sessionId: this.sessionId,
85-
fileOrDirectoryPath: parentItem.uri.replace(
86-
`/compute/sessions/${this.sessionId}/files/`,
87-
"",
88-
),
91+
fileOrDirectoryPath: this.trimComputePrefix(parentItem.uri),
8992
fileProperties: { name: folderName, isDirectory: true },
9093
});
9194

92-
return this.enrichWithDataProviderProperties(
93-
this.filePropertiesToContentItem(response.data),
94-
);
95+
return this.filePropertiesToContentItem(response.data);
9596
}
9697

9798
public async createNewItem(
@@ -101,18 +102,15 @@ class RestSASServerAdapter implements ContentAdapter {
101102
): Promise<ContentItem | undefined> {
102103
const response = await this.fileSystemApi.createFileOrDirectory({
103104
sessionId: this.sessionId,
104-
fileOrDirectoryPath: parentItem.uri.replace(
105-
`/compute/sessions/${this.sessionId}/files/`,
106-
"",
107-
),
105+
fileOrDirectoryPath: this.trimComputePrefix(parentItem.uri),
108106
fileProperties: { name: fileName, isDirectory: false },
109107
});
110108

111109
if (buffer) {
112110
const etag = response.headers.etag;
113-
const filePath = getLink(response.data.links, "GET", "self").uri.replace(
114-
`/compute/sessions/${this.sessionId}/files/`,
115-
"",
111+
// TODO (sas-server) This could be combined with update content most likely.
112+
const filePath = this.trimComputePrefix(
113+
getLink(response.data.links, "GET", "self").uri,
116114
);
117115
await this.fileSystemApi.updateFileContentOnSystem({
118116
sessionId: this.sessionId,
@@ -122,9 +120,7 @@ class RestSASServerAdapter implements ContentAdapter {
122120
});
123121
}
124122

125-
return this.enrichWithDataProviderProperties(
126-
this.filePropertiesToContentItem(response.data),
127-
);
123+
return this.filePropertiesToContentItem(response.data);
128124
}
129125

130126
public async deleteItem(item: ContentItem): Promise<boolean> {
@@ -134,60 +130,42 @@ class RestSASServerAdapter implements ContentAdapter {
134130
public async getChildItems(parentItem: ContentItem): Promise<ContentItem[]> {
135131
// If the user is fetching child items of the root folder, give them the
136132
// "home" directory
137-
const id = "SAS_SERVER_HOME_DIRECTORY";
138133
if (parentItem.id === SERVER_FOLDER_ID) {
139134
return [
140-
this.enrichWithDataProviderProperties({
141-
...createStaticFolder(
142-
id,
135+
this.filePropertiesToContentItem(
136+
createStaticFolder(
137+
SAS_SERVER_HOME_DIRECTORY,
143138
"Home",
144139
"userRoot",
145140
`/compute/sessions/${this.sessionId}/files/~fs~/members`,
146141
"getDirectoryMembers",
147142
),
148-
creationTimeStamp: 0,
149-
modifiedTimeStamp: 0,
150-
permission: undefined,
151-
}),
143+
),
152144
];
153145
}
154146

155147
const { data } = await this.fileSystemApi.getDirectoryMembers({
156148
sessionId: this.sessionId,
157-
directoryPath: parseMemberUri(
149+
directoryPath: this.trimComputePrefix(
158150
getLink(parentItem.links, "GET", "getDirectoryMembers").uri,
159-
this.sessionId,
160-
),
151+
).replace("/members", ""),
161152
});
162153

163154
// TODO (sas-server) We need to paginate and sort results
164155
return data.items.map((childItem: FileProperties, index) => ({
165156
...this.filePropertiesToContentItem(childItem),
166157
uid: `${parentItem.uid}/${index}`,
167-
...this.enrichWithDataProviderProperties(
168-
this.filePropertiesToContentItem(childItem),
169-
),
170158
}));
171-
172-
function parseMemberUri(uri: string, sessionId: string): string {
173-
return uri
174-
.replace(`/compute/sessions/${sessionId}/files/`, "")
175-
.replace("/members", "");
176-
}
177159
}
178160

179161
public async getContentOfItem(item: ContentItem): Promise<string> {
180162
throw new Error("getContentOfItem");
181163
}
182164

183165
public async getContentOfUri(uri: Uri): Promise<string> {
184-
// TODO (sas-server) We're using this a bunch. Make things more better-er
185-
const path = getResourceId(uri).replace(
186-
`/compute/sessions/${this.sessionId}/files/`,
187-
"",
188-
);
166+
const path = this.trimComputePrefix(getResourceId(uri));
189167

190-
const { data } = await this.fileSystemApi.getFileContentFromSystem(
168+
const response = await this.fileSystemApi.getFileContentFromSystem(
191169
{
192170
sessionId: this.sessionId,
193171
filePath: path,
@@ -197,11 +175,13 @@ class RestSASServerAdapter implements ContentAdapter {
197175
},
198176
);
199177

178+
this.updateFileMetadata(path, response);
179+
200180
// Disabling typescript checks on this line as this function is typed
201181
// to return AxiosResponse<void,any>. However, it appears to return
202182
// AxiosResponse<string,>.
203183
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
204-
return data as unknown as string;
184+
return response.data as unknown as string;
205185
}
206186

207187
public async getFolderPathForItem(item: ContentItem): Promise<string> {
@@ -214,18 +194,13 @@ class RestSASServerAdapter implements ContentAdapter {
214194

215195
public async getItemOfUri(uri: Uri): Promise<ContentItem> {
216196
const resourceId = getResourceId(uri);
197+
217198
const { data } = await this.fileSystemApi.getFileorDirectoryProperties({
218199
sessionId: this.sessionId,
219-
// TODO (sas-server) cleanup/reuse this
220-
fileOrDirectoryPath: resourceId.replace(
221-
`/compute/sessions/${this.sessionId}/files/`,
222-
"",
223-
),
200+
fileOrDirectoryPath: this.trimComputePrefix(resourceId),
224201
});
225202

226-
return this.enrichWithDataProviderProperties(
227-
this.filePropertiesToContentItem(data),
228-
);
203+
return this.filePropertiesToContentItem(data);
229204
}
230205

231206
public async getParentOfItem(
@@ -251,7 +226,7 @@ class RestSASServerAdapter implements ContentAdapter {
251226
this.rootFolders[delegateFolderName] = {
252227
...result.data,
253228
uid: `${index}`,
254-
...this.enrichWithDataProviderProperties(result.data),
229+
...this.filePropertiesToContentItem(result.data),
255230
};
256231
}
257232

@@ -293,56 +268,50 @@ class RestSASServerAdapter implements ContentAdapter {
293268
item: ContentItem,
294269
newName: string,
295270
): Promise<ContentItem | undefined> {
296-
throw new Error("Method not implemented.");
271+
const filePath = this.trimComputePrefix(item.uri);
272+
273+
const isDirectory = item.fileStat?.type === FileType.Directory;
274+
const parsedFilePath = filePath.split("~fs~");
275+
parsedFilePath.pop();
276+
const path = parsedFilePath.join("/");
277+
278+
const response = await this.fileSystemApi.updateFileOrDirectoryOnSystem({
279+
sessionId: this.sessionId,
280+
fileOrDirectoryPath: filePath,
281+
ifMatch: "",
282+
fileProperties: { name: newName, path, isDirectory },
283+
});
284+
285+
this.updateFileMetadata(filePath, response);
286+
287+
return this.filePropertiesToContentItem(response.data);
297288
}
298289

299290
public async restoreItem(item: ContentItem): Promise<boolean> {
300291
throw new Error("Method not implemented.");
301292
}
302293

303294
public async updateContentOfItem(uri: Uri, content: string): Promise<void> {
304-
throw new Error("Method not implemented.");
305-
}
295+
const filePath = this.trimComputePrefix(getResourceId(uri));
296+
const { etag } = this.getFileInfo(filePath);
306297

307-
private enrichWithDataProviderProperties(
308-
item: ContentItem,
309-
flags?: ContentItem["flags"],
310-
): ContentItem {
311-
item.flags = flags;
312-
return {
313-
...item,
314-
permission: getPermission(item),
315-
contextValue: resourceType(item),
316-
fileStat: {
317-
ctime: item.creationTimeStamp,
318-
mtime: item.modifiedTimeStamp,
319-
size: 0,
320-
type: getIsContainer(item) ? FileType.Directory : FileType.File,
321-
},
322-
isReference: isReference(item),
323-
resourceId: getResourceIdFromItem(item),
324-
vscUri: getSasServerUri(item, flags?.isInRecycleBin || false),
325-
typeName: getTypeName(item),
326-
};
298+
const response = await this.fileSystemApi.updateFileContentOnSystem({
299+
sessionId: this.sessionId,
300+
filePath,
301+
// updateFileContentOnSystem requires body to be a File type. However, the
302+
// underlying code is expecting a string. This forces compute to accept
303+
// a string.
304+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
305+
body: content as unknown as File,
306+
ifMatch: etag,
307+
});
327308

328-
function getIsContainer(item: ContentItem): boolean {
329-
if (item.fileStat?.type === FileType.Directory) {
330-
return true;
331-
}
332-
333-
const typeName = getTypeName(item);
334-
if (isItemInRecycleBin(item) && isReference(item)) {
335-
return false;
336-
}
337-
if (FOLDER_TYPES.indexOf(typeName) >= 0) {
338-
return true;
339-
}
340-
return false;
341-
}
309+
this.updateFileMetadata(filePath, response);
342310
}
343311

344312
private filePropertiesToContentItem(
345313
fileProperties: FileProperties,
314+
flags?: ContentItem["flags"],
346315
): ContentItem {
347316
const links = fileProperties.links.map((link) => ({
348317
method: link.method,
@@ -353,25 +322,65 @@ class RestSASServerAdapter implements ContentAdapter {
353322
}));
354323

355324
const id = getLink(links, "GET", "self").uri;
356-
return {
325+
const isRootFolder = [SERVER_FOLDER_ID, SAS_SERVER_HOME_DIRECTORY].includes(
326+
id,
327+
);
328+
const item = {
357329
id,
358330
uri: id,
359331
name: fileProperties.name,
360332
creationTimeStamp: 0,
361333
modifiedTimeStamp: new Date(fileProperties.modifiedTimeStamp).getTime(),
362334
links,
363-
// These will be overwritten
364335
permission: {
365-
write: false,
366-
delete: false,
367-
addMember: false,
336+
write: !isRootFolder && !fileProperties.readOnly,
337+
delete: !isRootFolder && !fileProperties.readOnly,
338+
addMember:
339+
!!getLink(links, "POST", "makeDirectory") ||
340+
!!getLink(links, "POST", "createFile"),
368341
},
342+
flags,
343+
};
344+
345+
const typeName = getTypeName(item);
346+
347+
return {
348+
...item,
349+
contextValue: resourceType(item),
369350
fileStat: {
370-
type: fileProperties.isDirectory ? FileType.Directory : FileType.File,
371-
ctime: 0,
372-
mtime: 0,
351+
ctime: item.creationTimeStamp,
352+
mtime: item.modifiedTimeStamp,
373353
size: 0,
354+
type:
355+
fileProperties.isDirectory ||
356+
FOLDER_TYPES.indexOf(typeName) >= 0 ||
357+
isRootFolder
358+
? FileType.Directory
359+
: FileType.File,
374360
},
361+
isReference: isReference(item),
362+
resourceId: getResourceIdFromItem(item),
363+
vscUri: getSasServerUri(item, flags?.isInRecycleBin || false),
364+
typeName: getTypeName(item),
365+
};
366+
}
367+
368+
private trimComputePrefix(uri: string): string {
369+
return uri.replace(`/compute/sessions/${this.sessionId}/files/`, "");
370+
}
371+
372+
private updateFileMetadata(id: string, { headers }: AxiosResponse) {
373+
this.fileMetadataMap[id] = {
374+
etag: headers.etag,
375+
};
376+
}
377+
378+
private getFileInfo(resourceId: string) {
379+
if (resourceId in this.fileMetadataMap) {
380+
return this.fileMetadataMap[resourceId];
381+
}
382+
return {
383+
etag: "",
375384
};
376385
}
377386
}

client/src/connection/rest/SASContentAdapter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,9 +340,9 @@ class SASContentAdapter implements ContentAdapter {
340340
flags?: ContentItem["flags"],
341341
): ContentItem {
342342
item.flags = flags;
343+
item.permission = getPermission(item);
343344
return {
344345
...item,
345-
permission: getPermission(item),
346346
contextValue: resourceType(item),
347347
fileStat: {
348348
ctime: item.creationTimeStamp,

0 commit comments

Comments
 (0)