Skip to content

Commit

Permalink
feat(uss/fs): copy/paste, write contents to LPAR, (wip) conflict mana…
Browse files Browse the repository at this point in the history
…gement

Signed-off-by: Trae Yelovich <[email protected]>
  • Loading branch information
traeok committed Oct 18, 2023
1 parent c9794d3 commit 0b8959c
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 80 deletions.
7 changes: 6 additions & 1 deletion packages/zowe-explorer-api/src/profiles/ZoweExplorerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export namespace ZoweExplorerApi {
* @param {string} ussFilePath
* @param {zowe.IDownloadOptions} options
*/
getContents(ussFilePath: string, options: zowe.IDownloadOptions): Promise<zowe.IZosFilesResponse | Buffer>;
getContents(ussFilePath: string, options: zowe.IDownloadSingleOptions): Promise<zowe.IZosFilesResponse>;

/**
* Uploads the file at the given path. Use for Save.
Expand All @@ -149,6 +149,11 @@ export namespace ZoweExplorerApi {
returnEtag?: boolean
): Promise<zowe.IZosFilesResponse>;

/**
* Uploads a given buffer as the contents of a file on USS.
*/
uploadBufferAsFile?(buffer: Buffer, filePath: string, options?: zowe.IUploadOptions): Promise<string | zowe.IZosFilesResponse>;

/**
* Uploads the file at the given path. Use for Save.
*
Expand Down
10 changes: 8 additions & 2 deletions packages/zowe-explorer-api/src/profiles/ZoweExplorerZosmfApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,20 @@ export class ZosmfUssApi extends ZosmfApiCommon implements ZoweExplorerApi.IUss
return zowe.Utilities.isFileTagBinOrAscii(this.getSession(), ussFilePath);
}

public getContents(inputFilePath: string, options: zowe.IGetOptions | zowe.IDownloadOptions): Promise<Buffer> {
return zowe.Get.USSFile(this.getSession(), inputFilePath, options);
public async getContents(inputFilePath: string, options: zowe.IDownloadSingleOptions): Promise<zowe.IZosFilesResponse> {
return zowe.Download.ussFile(this.getSession(), inputFilePath, {
...options,
});
}

public copy(outputPath: string, options?: Omit<object, "request">): Promise<Buffer> {
return zowe.Utilities.putUSSPayload(this.getSession(), outputPath, { ...(options ?? {}), request: "copy" });
}

public uploadBufferAsFile(buffer: Buffer, filePath: string, options?: zowe.IUploadOptions): Promise<string> {
return zowe.Upload.bufferToUssFile(this.getSession(), filePath, buffer, options);
}

/**
* API method to wrap to the newer `putContent`.
* @deprecated
Expand Down
4 changes: 2 additions & 2 deletions packages/zowe-explorer/src/uss/FileStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*/

import { ZoweLogger } from "../utils/LoggerUtils";
import * as vscode from "vscode";

/**
* File types within the USS tree structure
Expand All @@ -20,8 +21,7 @@ export enum UssFileType {
}

export interface UssFileTree {
// The path of the file on the local file system, if it exists
localPath?: string;
localUri?: vscode.Uri;

// The path of the file/directory as defined in USS
ussPath: string;
Expand Down
126 changes: 117 additions & 9 deletions packages/zowe-explorer/src/uss/UssFSProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import * as path from "path";
import * as vscode from "vscode";
import { Profiles } from "../Profiles";
import { ZoweExplorerApiRegister } from "../ZoweExplorerApiRegister";
import { UssFileTree, UssFileType } from "./FileStructure";
import { Duplex } from "stream";

export type FileEntryMetadata = {
profile: imperative.IProfileLoaded;
Expand All @@ -25,18 +27,21 @@ export interface UssEntry {
name: string;
metadata: FileEntryMetadata;
type: vscode.FileType;
wasAccessed: boolean;
}

export class UssFile implements UssEntry, vscode.FileStat {
public name: string;
public metadata: FileEntryMetadata;
public type: vscode.FileType;
public wasAccessed: boolean;

public ctime: number;
public mtime: number;
public size: number;
public binary: boolean;
public data?: Uint8Array;
public etag?: string;
public attributes: FileAttributes;

public constructor(name: string) {
Expand All @@ -46,19 +51,20 @@ export class UssFile implements UssEntry, vscode.FileStat {
this.size = 0;
this.name = name;
this.binary = false;
this.wasAccessed = false;
}
}

export class UssDirectory implements UssEntry, vscode.FileStat {
public name: string;
public metadata: FileEntryMetadata;
public type: vscode.FileType;
public wasAccessed: boolean;

public ctime: number;
public mtime: number;
public size: number;
public entries: Map<string, UssFile | UssDirectory>;
public wasAccessed: boolean;

public constructor(name: string) {
this.type = vscode.FileType.Directory;
Expand Down Expand Up @@ -196,24 +202,47 @@ export class UssFSProvider implements vscode.FileSystemProvider {
throw vscode.FileSystemError.FileNotFound("Session does not exist for this file.");
}

if (!file.data) {
if (!file.wasAccessed) {
// we need to fetch the contents from the mainframe since the file hasn't been accessed yet

class BufferBuilder extends Duplex {
private chunks: Uint8Array[];

public constructor() {
super();
this.chunks = [];
}

public _write(chunk: any, encoding: BufferEncoding, callback: (error?: Error) => void): void {
this.chunks.push(chunk);
callback();
}

public _read(size: number): void {
const concatBuf = Buffer.concat(this.chunks);
this.push(concatBuf);
this.push(null);
}
}

const bufBuilder = new BufferBuilder();
const filePath = uri.path.substring(startPathPos);
const resp = await ZoweExplorerApiRegister.getUssApi(loadedProfile).getContents(filePath, {
returnEtag: true,
encoding: loadedProfile.profile?.encoding,
responseTimeout: loadedProfile.profile?.responseTimeout,
stream: bufBuilder,
});
if (!(resp instanceof Buffer)) {
throw vscode.FileSystemError.FileNotFound();
}

file.data = resp;
file.data = bufBuilder.read();
file.etag = resp.apiResponse.etag;
file.wasAccessed = true;
}

return file.data;
}

public writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean }): void {
public async writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean }): Promise<void> {
/*
1. Parse URI to get file path for API endpoint
2. Make API call after assigning data to File object
Expand All @@ -239,11 +268,31 @@ export class UssFSProvider implements vscode.FileSystemProvider {
: this._getInfoFromUri(uri);
entry = new UssFile(basename);
entry.metadata = profInfo;
entry.data = content;
parent.entries.set(basename, entry);
this._fireSoon({ type: vscode.FileChangeType.Created, uri });
} else {
// TODO: we might want to use a callback or promise that returns data rather than saving the full contents in memory
entry.data = content.length > 0 ? content : null;
if (entry.wasAccessed) {
// entry was already accessed, this is an update to the existing file
// eslint-disable-next-line no-useless-catch
try {
await ZoweExplorerApiRegister.getUssApi(parent.metadata.profile).uploadBufferAsFile(
Buffer.from(content),
entry.metadata.ussPath,
{ etag: entry.etag }
);
} catch (err) {
// TODO: conflict management
// if (err.message.includes("Rest API failure with HTTP(S) status 412")) {
// Gui.errorMessage("There is a newer version of this file on the mainframe. Compare with remote contents or overwrite?");
// }
throw err;
}
entry.data = content;
} else {
entry.data = content;
}
// if the entry hasn't been accessed yet, we don't need to call the API since we are just creating the file
}
entry.mtime = Date.now();
entry.size = content.byteLength;
Expand Down Expand Up @@ -331,6 +380,65 @@ export class UssFSProvider implements vscode.FileSystemProvider {
this._fireSoon({ type: vscode.FileChangeType.Changed, uri: dirname }, { uri, type: vscode.FileChangeType.Deleted });
}

public async copyEx(
source: vscode.Uri,
destination: vscode.Uri,
options: { readonly overwrite: boolean; readonly tree: UssFileTree }
): Promise<void> {
const destInfo = this._getInfoFromUri(destination);
const sourceInfo = this._getInfoFromUri(source);
const api = ZoweExplorerApiRegister.getUssApi(destInfo.profile);

const hasCopyApi = api.copy != null;

const apiResponse = await api.fileList(destInfo.ussPath);
const fileList = apiResponse.apiResponse?.items;

// Check root path for conflicts before pasting nodes in this path
let fileName = path.basename(sourceInfo.ussPath);
if (fileList?.find((file) => file.name === fileName) != null) {
// If file names match, build the copy suffix
let dupCount = 1;
const extension = path.extname(fileName);
const baseNameForFile = path.parse(fileName)?.name;
let dupName = `${baseNameForFile} (${dupCount})${extension}`;
while (fileList.find((file) => file.name === dupName) != null) {
dupCount++;
dupName = `${baseNameForFile} (${dupCount})${extension}`;
}
fileName = dupName;
}
const outputPath = `${destInfo.ussPath}/${fileName}`;

if (hasCopyApi && sourceInfo.profile.profile === destInfo.profile.profile) {
await api.copy(outputPath, {
from: sourceInfo.ussPath,
recursive: options.tree.type === UssFileType.Directory,
overwrite: options.overwrite ?? true,
});
} else if (options.tree.type === UssFileType.Directory) {
// Not all APIs respect the recursive option, so it's best to
// recurse within this operation to avoid missing files/folders
await api.create(outputPath, "directory");
if (options.tree.children) {
for (const child of options.tree.children) {
await this.copyEx(child.localUri, vscode.Uri.parse(`uss:${outputPath}`), { ...options, tree: child });
}
}
} else {
const fileEntry = this._lookup(source, true);
if (fileEntry == null) {
return;
}

if (!fileEntry.wasAccessed) {
// must fetch contents of file first before pasting in new path
const fileContents = await vscode.workspace.fs.readFile(source);
await api.uploadBufferAsFile(Buffer.from(fileContents), outputPath);
}
}
}

public createDirectory(uri: vscode.Uri): void {
/*
1. Parse URI to get desired directory path
Expand Down
70 changes: 9 additions & 61 deletions packages/zowe-explorer/src/uss/ZoweUSSNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { closeOpenedTextFile } from "../utils/workspace";
import * as nls from "vscode-nls";
import { UssFileTree, UssFileType, UssFileUtils } from "./FileStructure";
import { ZoweLogger } from "../utils/LoggerUtils";
import { UssFile } from "./UssFSProvider";
import { UssFile, UssFSProvider } from "./UssFSProvider";
import { USSTree } from "./USSTree";

// Set up localization
Expand Down Expand Up @@ -670,71 +670,19 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode {
*/
public async paste(
sessionName: string,
rootPath: string,
destUri: vscode.Uri,
uss: { tree: UssFileTree; api: ZoweExplorerApi.IUss; options?: IUploadOptions }
): Promise<void> {
ZoweLogger.trace("ZoweUSSNode.paste called.");
const hasCopyApi = uss.api.copy != null;
const hasPutContentApi = uss.api.putContent != null;
if (!uss.api.fileList || (!hasCopyApi && !hasPutContentApi)) {
throw new Error(localize("paste.missingApis", "Required API functions for pasting (fileList, copy and/or putContent) were not found."));
}

const apiResponse = await uss.api.fileList(rootPath);
const fileList = apiResponse.apiResponse?.items;

// Check root path for conflicts before pasting nodes in this path
let fileName = uss.tree.baseName;
if (fileList?.find((file) => file.name === fileName) != null) {
// If file names match, build the copy suffix
let dupCount = 1;
const extension = path.extname(uss.tree.baseName);
const baseNameForFile = path.parse(uss.tree.baseName)?.name;
let dupName = `${baseNameForFile} (${dupCount})${extension}`;
while (fileList.find((file) => file.name === dupName) != null) {
dupCount++;
dupName = `${baseNameForFile} (${dupCount})${extension}`;
}
fileName = dupName;
if (!uss.api.fileList || !hasCopyApi) {
throw new Error(localize("paste.missingApis", "Required API functions for pasting (fileList and copy) were not found."));
}
const outputPath = `${rootPath}/${fileName}`;

if (hasCopyApi && UssFileUtils.toSameSession(uss.tree, sessionName)) {
await uss.api.copy(outputPath, {
from: uss.tree.ussPath,
recursive: uss.tree.type === UssFileType.Directory,
});
} else {
const existsLocally = fs.existsSync(uss.tree.localPath);
switch (uss.tree.type) {
case UssFileType.Directory:
if (!existsLocally) {
// We will need to build the file structure locally, to pull files down if needed
fs.mkdirSync(uss.tree.localPath, { recursive: true });
}
// Not all APIs respect the recursive option, so it's best to
// recurse within this operation to avoid missing files/folders
await uss.api.create(outputPath, "directory");
if (uss.tree.children) {
for (const child of uss.tree.children) {
await this.paste(sessionName, outputPath, { api: uss.api, tree: child, options: uss.options });
}
}
break;
case UssFileType.File:
if (!existsLocally) {
await uss.api.getContents(uss.tree.ussPath, {
file: uss.tree.localPath,
binary: uss.tree.binary,
returnEtag: true,
encoding: this.profile.profile?.encoding,
responseTimeout: this.profile.profile?.responseTimeout,
});
}
await uss.api.putContent(uss.tree.localPath, outputPath, uss.options);
break;
}
}
await UssFSProvider.instance.copyEx(uss.tree.localUri, destUri, {
overwrite: true,
tree: uss.tree,
});
}

/**
Expand Down Expand Up @@ -766,7 +714,7 @@ export class ZoweUSSNode extends ZoweTreeNode implements IZoweUSSTreeNode {
};

for (const subnode of fileTreeToPaste.children) {
await this.paste(sessionName, remotePath, { api, tree: subnode, options });
await this.paste(sessionName, vscode.Uri.parse(`uss:/${this.profile.name}${this.fullPath}`), { api, tree: subnode, options });
}
} catch (error) {
await errorHandling(error, this.label.toString(), localize("copyUssFile.error", "Error uploading files"));
Expand Down
9 changes: 4 additions & 5 deletions packages/zowe-explorer/src/uss/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ export async function buildFileStructure(node: IZoweUSSTreeNode): Promise<UssFil
ZoweLogger.trace("uss.actions.buildFileStructure called.");
if (contextually.isUssDirectory(node)) {
const directory: UssFileTree = {
localPath: node.getUSSDocumentFilePath(),
localUri: node.uri,
ussPath: node.fullPath,
baseName: node.getLabel() as string,
sessionName: node.getSessionNode().getLabel() as string,
Expand All @@ -380,7 +380,7 @@ export async function buildFileStructure(node: IZoweUSSTreeNode): Promise<UssFil
return {
children: [],
binary: node.binary,
localPath: node.getUSSDocumentFilePath(),
localUri: node.uri,
ussPath: node.fullPath,
baseName: node.getLabel() as string,
sessionName: node.getSessionNode().getLabel() as string,
Expand Down Expand Up @@ -487,9 +487,8 @@ export async function pasteUss(ussFileProvider: IZoweTree<IZoweUSSTreeNode>, nod
title: localize("ZoweUssNode.copyUpload.progress", "Pasting files..."),
},
async () => {
await (selectedNode.pasteUssTree ? selectedNode.pasteUssTree() : selectedNode.copyUssFile());
await selectedNode.pasteUssTree();
}
);
const nodeToRefresh = node?.contextValue != null && contextually.isUssSession(node) ? selectedNode : selectedNode.getParent();
ussFileProvider.refreshElement(nodeToRefresh);
ussFileProvider.refreshElement(selectedNode);
}

0 comments on commit 0b8959c

Please sign in to comment.