-
Notifications
You must be signed in to change notification settings - Fork 118
Expand file tree
/
Copy pathZoweExplorerFtpUssApi.ts
More file actions
333 lines (310 loc) · 13.7 KB
/
Copy pathZoweExplorerFtpUssApi.ts
File metadata and controls
333 lines (310 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
/**
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
import * as fs from "fs";
import * as path from "path";
import * as crypto from "crypto";
import * as os from "os";
import * as zosfiles from "@zowe/zos-files-for-zowe-sdk";
import { CoreUtils, UssUtils, TransferMode } from "@zowe/zos-ftp-for-zowe-cli";
import { BufferBuilder, imperative, MainframeInteraction, MessageSeverity } from "@zowe/zowe-explorer-api";
import { Buffer } from "buffer";
import { AbstractFtpApi } from "./ZoweExplorerAbstractFtpApi";
import { ZoweFtpExtensionError } from "./ZoweFtpExtensionError";
import { LOGGER } from "./globals";
// The Zowe FTP CLI plugin is written and uses mostly JavaScript, so relax the rules here.
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
export class FtpUssApi extends AbstractFtpApi implements MainframeInteraction.IUss {
public async fileList(ussFilePath: string): Promise<zosfiles.IZosFilesResponse> {
const result = this.getDefaultResponse();
const session = this.getSession(this.profile);
try {
if (!session.ussListConnection?.isConnected()) {
session.ussListConnection = await this.ftpClient(this.checkedProfile());
}
if (session.ussListConnection.isConnected()) {
const response = await UssUtils.listFiles(session.ussListConnection, ussFilePath);
if (response) {
result.success = true;
result.apiResponse.items = response.map((element) => ({
name: element.name,
user: element.owner,
group: element.group,
size: element.size,
mtime: element.lastModified,
mode: element.permissions,
}));
}
// Assume the path is a directory if response contains zero or
// multiple items, or a single item with relative path.
const isDir = response.length !== 1 || response[0].name !== ussFilePath;
if (isDir) {
result.apiResponse.items.unshift({ name: ".", mode: "d" }, { name: "..", mode: "d" });
}
}
return result;
} catch (err) {
throw new ZoweFtpExtensionError(err.message);
}
}
// eslint-disable-next-line @typescript-eslint/require-await, require-await
public async isFileTagBinOrAscii(_ussFilePath: string): Promise<boolean> {
return false; // TODO: needs to be implemented checking file type
}
public async getContents(ussFilePath: string, options: zosfiles.IDownloadSingleOptions): Promise<zosfiles.IZosFilesResponse> {
const result = this.getDefaultResponse();
const transferOptions = {
transferType: CoreUtils.getBinaryTransferModeOrDefault(options.binary),
localFile: options.file,
size: 1,
};
const fileOrStreamSpecified = options.file != null || options.stream != null;
let connection;
try {
connection = await this.ftpClient(this.checkedProfile());
if (!connection || !fileOrStreamSpecified) {
LOGGER.logImperativeMessage(result.commandResponse, MessageSeverity.ERROR);
throw new Error(result.commandResponse);
}
if (options.file) {
imperative.IO.createDirsSyncFromFilePath(options.file);
await UssUtils.downloadFile(connection, ussFilePath, transferOptions);
result.success = true;
result.commandResponse = "";
result.apiResponse.etag = await this.hashFile(options.file);
} else if (options.stream) {
const buffer = await UssUtils.downloadFile(connection, ussFilePath, transferOptions);
result.apiResponse.etag = this.hashBuffer(buffer);
options.stream.write(buffer);
options.stream.end();
}
return result;
} catch (err) {
throw new ZoweFtpExtensionError(err.message);
} finally {
this.releaseConnection(connection);
}
}
/**
* Uploads a USS file from the given buffer.
* @param buffer The buffer containing the contents of the USS file
* @param filePath The path for the USS file
* @param options Any options for the upload
*
* @returns A file response with the results of the upload operation.
*/
public async uploadFromBuffer(buffer: Buffer, filePath: string, options?: zosfiles.IUploadOptions): Promise<zosfiles.IZosFilesResponse> {
const result = await this.putContent(buffer, filePath, options);
return result;
}
/**
* Upload a file (located at the input path) to the destination path.
*
* @param input The input file path or buffer to upload
* @param ussFilePath The destination file path on USS
* @param options Any options for the upload
*
* @returns A file response containing the results of the operation.
*/
public async putContent(input: string | Buffer, ussFilePath: string, options?: zosfiles.IUploadOptions): Promise<zosfiles.IZosFilesResponse> {
const inputIsBuffer = input instanceof Buffer;
const transferOptions = {
content: inputIsBuffer ? input : undefined,
localFile: (inputIsBuffer ? undefined : input) as string,
transferType: CoreUtils.getBinaryTransferModeOrDefault(options?.binary),
};
const result = this.getDefaultResponse();
// Save-Save with FTP requires loading the file first
// (moved this block above connection request so only one connection is active at a time)
if (options?.returnEtag && options.etag) {
const contentsTag = await this.getContentsTag(ussFilePath, inputIsBuffer);
if (contentsTag && contentsTag !== options.etag) {
throw new Error("Rest API failure with HTTP(S) status 412 Save conflict.");
}
}
let connection;
try {
connection = await this.ftpClient(this.checkedProfile());
if (!connection) {
throw new Error(result.commandResponse);
}
await UssUtils.uploadFile(connection, ussFilePath, transferOptions);
result.success = true;
if (options?.returnEtag) {
// release this connection instance because a new one will be made with getContentsTag
this.releaseConnection(connection);
connection = null;
const contentsTag = await this.getContentsTag(ussFilePath, inputIsBuffer);
result.apiResponse.etag = contentsTag;
}
result.commandResponse = "File uploaded successfully.";
return result;
} catch (err) {
throw new ZoweFtpExtensionError(err.message);
} finally {
this.releaseConnection(connection);
}
}
public async uploadDirectory(
inputDirectoryPath: string,
ussDirectoryPath: string,
_options: zosfiles.IUploadOptions
): Promise<zosfiles.IZosFilesResponse> {
let result = this.getDefaultResponse();
try {
// Check if inputDirectory is directory
if (!imperative.IO.isDir(inputDirectoryPath)) {
throw new ZoweFtpExtensionError("The local directory path provided does not exist.");
}
// Make directory before copying inner files
await this.putContent(inputDirectoryPath, ussDirectoryPath);
// getting list of files from directory
const files = zosfiles.ZosFilesUtils.getFileListFromPath(inputDirectoryPath, true);
// TODO: this solution will not perform very well; rewrite this and putContent methods
for (const file of files) {
const relativePath = path.relative(inputDirectoryPath, file).replace(/\\/g, "/");
const putResult = await this.putContent(file, path.posix.join(ussDirectoryPath, relativePath));
result = putResult;
}
return result;
} catch (err) {
throw new ZoweFtpExtensionError(err.message);
}
}
public async create(ussPath: string, type: string, _mode?: string): Promise<zosfiles.IZosFilesResponse> {
const result = this.getDefaultResponse();
let connection;
try {
connection = await this.ftpClient(this.checkedProfile());
if (connection) {
if (type === "directory") {
await UssUtils.makeDirectory(connection, ussPath);
} else if (type === "File" || type === "file") {
const content = Buffer.from(CoreUtils.addCarriageReturns(""));
const transferOptions = {
transferType: TransferMode.ASCII,
content: content,
};
await UssUtils.uploadFile(connection, ussPath, transferOptions);
}
result.success = true;
result.commandResponse = "Directory or file created.";
} else {
throw new Error(result.commandResponse);
}
return result;
} catch (err) {
throw new ZoweFtpExtensionError(err.message);
} finally {
this.releaseConnection(connection);
}
}
public async delete(ussPath: string, recursive?: boolean): Promise<zosfiles.IZosFilesResponse> {
const result = this.getDefaultResponse();
let connection;
try {
connection = await this.ftpClient(this.checkedProfile());
if (connection) {
if (recursive) {
await this.deleteDirectory(ussPath, connection);
} else {
await UssUtils.deleteFile(connection, ussPath);
}
result.success = true;
result.commandResponse = "Delete completed.";
} else {
throw new Error(result.commandResponse);
}
return result;
} catch (err) {
throw new ZoweFtpExtensionError(err.message);
} finally {
this.releaseConnection(connection);
}
}
public async rename(currentUssPath: string, newUssPath: string): Promise<zosfiles.IZosFilesResponse> {
const result = this.getDefaultResponse();
let connection;
try {
connection = await this.ftpClient(this.checkedProfile());
if (connection) {
await UssUtils.renameFile(connection, currentUssPath, newUssPath);
result.success = true;
result.commandResponse = "Rename completed.";
} else {
throw new Error(result.commandResponse);
}
return result;
} catch (err) {
throw new ZoweFtpExtensionError(err.message);
} finally {
this.releaseConnection(connection);
}
}
private async deleteDirectory(ussPath: string, connection): Promise<void> {
const result = this.getDefaultResponse();
try {
await UssUtils.deleteDirectory(connection, ussPath);
result.success = true;
result.commandResponse = "Delete Completed";
} finally {
this.releaseConnection(connection);
}
}
private async getContentsTag(ussFilePath: string, buffer?: boolean): Promise<string> {
if (buffer) {
const writable = new BufferBuilder();
const loadResult = await this.getContents(ussFilePath, { stream: writable });
return loadResult.apiResponse.etag as string;
}
// Create a temporary directory and unique filename
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "zowe-ftp-uss-"));
const tmpFileName = path.join(tmpDir, `temp-${crypto.randomUUID()}.dat`);
try {
const options: zosfiles.IDownloadOptions = {
binary: false,
file: tmpFileName,
};
const loadResult = await this.getContents(ussFilePath, options);
return loadResult.apiResponse.etag as string;
} finally {
// Clean up temporary file and directory
try {
fs.rmSync(tmpDir, { force: true, recursive: true });
} catch (cleanupError) {
if (cleanupError instanceof Error) {
LOGGER.logImperativeMessage(`Failed to clean up temporary files: ${cleanupError.message}`, MessageSeverity.WARN);
}
}
}
}
private getDefaultResponse(): zosfiles.IZosFilesResponse {
return {
success: false,
commandResponse: "Could not get a valid FTP connection.",
apiResponse: {},
};
}
private hashFile(filename: string): Promise<string> {
return new Promise((resolve) => {
const hash = crypto.createHash("sha256");
const input = fs.createReadStream(filename);
input.on("readable", () => {
const data = input.read();
if (data) {
hash.update(data as unknown as crypto.BinaryLike);
} else {
resolve(`${hash.digest("hex")}`);
}
});
});
}
}