Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/zosfiles/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to the Zowe z/OS files SDK package will be documented in this file.

## Recent Changes

- BugFix: Fixed an issue where if a file fails to download and one already exists in the destination, the old file is deleted. [#2617](https://github.com/zowe/zowe-cli/pull/2617)

## `8.27.4`

- BugFix: Updated minimum supported version of Node from 18 to 20. Added Node 24 support. [#2616](https://github.com/zowe/zowe-cli/pull/2616)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,68 @@ describe("z/OS Files - Download", () => {
expect(ioCreateDirSpy).toHaveBeenCalledWith(destination);
expect(ioWriteStreamSpy).toHaveBeenCalledTimes(1);
});

it("should not delete existing file when download fails", async () => {
let response;
let caughtError;
const file = "existing/file.txt";
const ioExistsSpy = jest.spyOn(IO, "existsSync");
const ioDeleteSpy = jest.spyOn(IO, "deleteFile");

ioExistsSpy.mockReturnValue(true);
ioDeleteSpy.mockImplementation(() => null);

const dummyError = new Error("Connection lost");
zosmfGetFullSpy.mockImplementation(() => {
throw dummyError;
});

try {
response = await Download.dataSet(dummySession, dsname, { file });
} catch (e) {
caughtError = e;
}

expect(response).toBeUndefined();
expect(caughtError).toEqual(dummyError);

expect(ioExistsSpy).toHaveBeenCalledWith(file);
expect(ioDeleteSpy).not.toHaveBeenCalled();

ioExistsSpy.mockRestore();
ioDeleteSpy.mockRestore();
});

it("should delete new file when download fails", async () => {
let response;
let caughtError;
const file = "new/file.txt";
const ioExistsSpy = jest.spyOn(IO, "existsSync");
const ioDeleteSpy = jest.spyOn(IO, "deleteFile");

ioExistsSpy.mockReturnValue(false);
ioDeleteSpy.mockImplementation(() => null);

const dummyError = new Error("Connection lost");
zosmfGetFullSpy.mockImplementation(() => {
throw dummyError;
});

try {
response = await Download.dataSet(dummySession, dsname, { file });
} catch (e) {
caughtError = e;
}

expect(response).toBeUndefined();
expect(caughtError).toEqual(dummyError);

expect(ioExistsSpy).toHaveBeenCalledWith(file);
expect(ioDeleteSpy).toHaveBeenCalledWith(file);

ioExistsSpy.mockRestore();
ioDeleteSpy.mockRestore();
});
});

describe("allMembers", () => {
Expand Down Expand Up @@ -1222,6 +1284,76 @@ describe("z/OS Files - Download", () => {
}
);
});

it("should not delete existing member files when download fails", async () => {
let response;
let caughtError;
const ioExistsSpy = jest.spyOn(IO, "existsSync");
const ioDeleteSpy = jest.spyOn(IO, "deleteFile");

ioExistsSpy.mockImplementation((filePath) => {
const fileName = path.basename(filePath.toString());
return fileName === "m1.txt" || fileName === "m2.txt";
});
ioDeleteSpy.mockImplementation(() => null);

const dummyError = new Error("Connection lost");
downloadDatasetSpy.mockImplementation(async () => {
throw dummyError;
});

try {
response = await Download.allMembers(dummySession, dsname, { failFast: false });
} catch (e) {
caughtError = e;
}

expect(response).toBeUndefined();
expect(caughtError).toBeDefined();
expect(caughtError.message).toContain(ZosFilesMessages.memberDownloadFailed.message);

expect(ioExistsSpy).toHaveBeenCalledWith(path.posix.join(dsFolder, "m1.txt"));
expect(ioExistsSpy).toHaveBeenCalledWith(path.posix.join(dsFolder, "m2.txt"));

expect(ioDeleteSpy).not.toHaveBeenCalled();

ioExistsSpy.mockRestore();
ioDeleteSpy.mockRestore();
});

it("should delete new member files when download fails", async () => {
let response;
let caughtError;
const ioExistsSpy = jest.spyOn(IO, "existsSync");
const ioDeleteSpy = jest.spyOn(IO, "deleteFile");

ioExistsSpy.mockReturnValue(false);
ioDeleteSpy.mockImplementation(() => null);

const dummyError = new Error("Connection lost");
downloadDatasetSpy.mockImplementation(async () => {
throw dummyError;
});

try {
response = await Download.allMembers(dummySession, dsname, { failFast: false });
} catch (e) {
caughtError = e;
}

expect(response).toBeUndefined();
expect(caughtError).toBeDefined();
expect(caughtError.message).toContain(ZosFilesMessages.memberDownloadFailed.message);

expect(ioExistsSpy).toHaveBeenCalledWith(path.posix.join(dsFolder, "m1.txt"));
expect(ioExistsSpy).toHaveBeenCalledWith(path.posix.join(dsFolder, "m2.txt"));

expect(ioDeleteSpy).toHaveBeenCalledWith(path.posix.join(dsFolder, "m1.txt"));
expect(ioDeleteSpy).toHaveBeenCalledWith(path.posix.join(dsFolder, "m2.txt"));

ioExistsSpy.mockRestore();
ioDeleteSpy.mockRestore();
});
});

describe("allDataSets", () => {
Expand Down
33 changes: 28 additions & 5 deletions packages/zosfiles/src/methods/download/Download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ export class Download {
ImperativeExpect.toNotBeNullOrUndefined(dataSetName, ZosFilesMessages.missingDatasetName.message);
ImperativeExpect.toNotBeEqual(dataSetName, "", ZosFilesMessages.missingDatasetName.message);
let destination: string;
let existedBefore = false;
let downloadStarted = false;

try {
// Format the endpoint to send the request to
Expand Down Expand Up @@ -125,6 +127,9 @@ export class Download {
return generatedFilePath + IO.normalizeExtension(extension);
})();

// Track if a file at destination already exists before staring the download
existedBefore = IO.existsSync(destination);

IO.createDirsSyncFromFilePath(destination);
}

Expand All @@ -149,6 +154,8 @@ export class Download {
requestOptions.dataToReturn = [CLIENT_PROPERTY.response];
}

downloadStarted = true;

const request: IRestClientResponse = await ZosmfRestClient.getExpectFullResponse(session, requestOptions);

// By default, apiResponse is empty when downloading
Expand All @@ -168,8 +175,13 @@ export class Download {
} catch (error) {
Logger.getAppLogger().error(error);

if (destination != null) {
IO.deleteFile(destination);
// Only delete the file if it did not exist before and error occured during download
if (destination != null && downloadStarted && !existedBefore) {
try {
IO.deleteFile(destination);
} catch (deleteError) {
Logger.getAppLogger().warn(`Failed to clean up partially download file ${destination}: ${deleteError.message}`);
}
}

throw error;
Expand Down Expand Up @@ -271,18 +283,29 @@ export class Download {
extension = extension.replace(/^\.+/g, "");
}

const memberFilePath = posix.join(baseDir, fileName + IO.normalizeExtension(extension));
const memberExistedBefore = IO.existsSync(memberFilePath);

return this.dataSet(session, `${dataSetName}(${mem.member})`, {
volume: options.volume,
file: posix.join(baseDir, fileName + IO.normalizeExtension(extension)),
file: memberFilePath,
binary: options.binary,
record: options.record,
encoding: options.encoding,
responseTimeout: options.responseTimeout
}).catch((err) => {
downloadErrors.push(err);
failedMembers.push(fileName);
// Delete the file that could not be downloaded
IO.deleteFile(join(baseDir, fileName + IO.normalizeExtension(extension)));

// Only delete the file if it didn't exist before the download attempt
if (!memberExistedBefore) {
try {
IO.deleteFile(memberFilePath);
} catch (deleteError) {
Logger.getAppLogger().warn(`Failed to clean up partially download file ${memberFilePath}: ${deleteError.message}`);
}
}

// If we should fail fast, rethrow error
if (options.failFast || options.failFast === undefined) {
throw err;
Expand Down