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
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**Read [the guideline](https://github.com/mate-academy/js_task-guideline/blob/master/README.md) before start**

Write an app that will move a file from one location to another like Linux mv
Write an app that will move a file from one location to another like Linux (or Mac) mv
command: `mv file.txt ./someDir/` (this will create `file.txt` in `./someDir`
and remove the source file).
- If a destination contains `/` in the end it must be considered as a directory.
Expand Down
81 changes: 81 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1 +1,82 @@
/* eslint-disable no-console */
// write code here
'use strict';

const fs = require('fs/promises');
const path = require('path');

async function move(oldPath, newPath) {
try {
if (!oldPath || !newPath) {
throw new Error('Exactly two arguments are required');
}

Comment on lines +9 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: The move function wraps its entire body in a try/catch and the catch only logs the error. This swallows errors and prevents the CLI wrapper from receiving a rejected promise. Remove the top-level try/catch here (or rethrow the error after logging) so callers can handle errors and the process can exit with non-zero. This is required so tests and the CLI detect failures (see checklist items about exiting non-zero on errors).

if (oldPath === newPath) {
return;
}

const srcStats = await fs.stat(oldPath);

if (!srcStats.isFile()) {
throw new Error('Source is not a file');
}
Comment on lines +18 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation: your source validation (stat + isFile()) is correct. However, if fs.stat(oldPath) throws (source missing), that error must propagate to the CLI wrapper so the process can exit non-zero. After removing the swallow-catch the existing logic will correctly cause a rejection on missing source.

Comment on lines +18 to +22

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source validation is correct — ensure errors are not swallowed.

You call fs.stat(oldPath) and verify isFile() (lines 18–22), which matches the requirement to only support files as sources. Ensure that ENOENT or the Source is not a file error is allowed to propagate to the caller (remove the top-level suppression) so the CLI exits non-zero on invalid sources.


const trailing = newPath.endsWith(path.sep) || newPath.endsWith('/');
let dest;

if (trailing) {
const destStats = await fs.stat(newPath);

if (!destStats.isDirectory()) {
throw new Error('Destination is not a directory');
}

dest = path.join(newPath, path.basename(oldPath));
Comment on lines +27 to +34

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing-slash directory handling is implemented but errors must propagate.

The trailing-slash branch (lines 27–34) correctly attempts fs.stat(newPath) and verifies it's a directory. However, because the outer catch swallows errors, a missing directory will only be logged, not cause the process to fail. Once you remove/rework the swallow-catch, the ENOENT will correctly surface as required by the spec.

} else {
Comment on lines +24 to +35

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing-slash handling: you correctly detect trailing slash with newPath.endsWith(path.sep) || newPath.endsWith('/'). When trailing is true you stat newPath; ensure that if stat throws because the directory does not exist you allow that error to propagate (and cause a non-zero exit) rather than only logging it. With the top-level catch removed this will behave as required by the spec.

try {
const destStats = await fs.stat(newPath);

if (destStats.isDirectory()) {
dest = path.join(newPath, path.basename(oldPath));
} else {
dest = newPath;
}
Comment on lines +36 to +43

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check for existing destination to avoid platform-dependent overwrite.

Before performing the rename/copy you should explicitly check whether dest exists and fail with a clear error (for example: Destination already exists). The current code calculates dest but does not check for its existence prior to fs.rename (see block where dest is determined, lines 36–43). Add a fs.stat(dest) or fs.access(dest) check and throw if it exists so behavior is deterministic and tests won't depend on platform rename semantics.

} catch (err) {
if (err.code === 'ENOENT') {
const destDir = path.dirname(newPath);
const dirStats = await fs.stat(destDir);

if (!dirStats.isDirectory()) {
throw new Error('Destination directory does not exist');
}

dest = newPath;
Comment on lines +44 to +53

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Destination parent-directory check is correct but must propagate.

When fs.stat(newPath) throws ENOENT you check the parent directory and throw Destination directory does not exist (lines 44–53). This matches checklist requirements — keep this logic but ensure the error reaches the caller (remove the outer swallow-catch).

} else {
throw err;
}
}
}

await fs.rename(oldPath, dest);
console.log(`Moved ${oldPath} to ${dest}`);
Comment on lines +59 to +61

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Functional gap: fs.rename is invoked directly. On cross-device moves rename can fail with code === 'EXDEV'. Implement a fallback: on EXDEV do a copy (fs.copyFile or streaming copy) from oldPath to dest, then unlink(oldPath) only after a successful copy. Also ensure you remove any partially copied dest if the copy fails. This is required to match mv behavior across filesystems.

Comment on lines +59 to +61

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overwrite safety: before renaming/copying, check whether the computed dest already exists. The assignment expects you not to silently overwrite; either fail with an error or rely on rename to error consistently. It's safer to stat dest and throw if it exists rather than risking platform-dependent overwrite behavior.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The success console.log(Moved ${oldPath} to ${dest}) prints output on successful moves. The specification states no extra output is required on success. Remove this line to keep the utility silent on success.

Comment on lines +58 to +61

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs.rename has no EXDEV fallback — add copy+unlink on cross-device errors.

You call await fs.rename(oldPath, dest) (line 60) without handling err.code === 'EXDEV'. Implement a try/catch around fs.rename; if the error code is EXDEV, perform a safe copy (e.g., fs.copyFile(oldPath, dest) or stream copy) and only await fs.unlink(oldPath) after a successful copy. Also ensure that if copy fails you remove any partially-created dest file to avoid leaving partial results. This satisfies the cross-device handling requirement in the checklist.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not log success — tool should be silent on success.

Remove the console.log('Moved ...') on line 61. The program should produce no output when the operation succeeds (per project expectations).

} catch (err) {
console.error('Move error:', err.message);
Comment on lines +62 to +63

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because of the catch above, thrown errors (e.g., missing source, missing destination directory) are printed but not propagated. The catch block should either rethrow the error or be removed. Currently this behavior prevents the CLI wrapper's .catch from executing. Let errors propagate so the process can exit 1 when move fails.

}
Comment on lines +9 to +64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Top-level error swallowing in move() — prevents callers from observing failures.

You wrap the entire move body in a try/catch that only logs the error and does not rethrow (lines 9–64). Per the specification the function must reject on error so the CLI and tests can observe a failure and exit non-zero. Remove this outer try/catch or rethrow the error after logging so callers receive the rejection. This addresses checklist items about error handling and CLI exit behavior.

}

if (require.main === module) {
const args = process.argv.slice(2);

if (args.length !== 2) {
console.error('Exactly two arguments are required');
}
Comment on lines +70 to +72

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the CLI is invoked with a number of args different than two, the code logs an error but does not stop execution. After the error message the script continues, destructures args and calls move with possibly undefined values. Requirement: the app must be executable via node index with exactly two arguments (checklist items 1.2 and 2.2). Change: after logging the error you should exit/return immediately (for example, call process.exit(1) or return) so the program does not continue. This enforces the "exactly two arguments" rule and avoids confusing follow-up errors.

Comment on lines +70 to +72

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLI argument enforcement: you log an error when args.length !== 2 but do not stop execution. Per the spec you must enforce exactly two positional arguments and exit non-zero if the count is incorrect. Call process.exit(1) (or return after exiting) here and do not call move when the arguments are invalid.

Comment on lines +70 to +72

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CLI argument validation must exit immediately on invalid usage.

At lines 70–72 you log when args.length !== 2 but then continue to call move. The program should not proceed — call process.exit(1) (or throw) immediately after logging to ensure a non-zero exit on invalid usage, as required by the task.


const [oldPath, newPath] = args;

move(oldPath, newPath).catch((err) => {
console.error(err);
process.exit(1);
});
Comment on lines +76 to +79

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CLI wrapper attaches a .catch to move(...) expecting rejections to be handled, but because move currently swallows errors this catch will rarely fire. Ensure move rejects on error (remove swallowing catch) so this .catch can call process.exit(1) and signal failure to the caller/test harness.

}

module.exports = { move };

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

module.exports is fine — keep move async and rejecting on error for tests.

module.exports = { move } (line 82) is correct. Tests or consumers will expect move() to reject on error; ensure move does not swallow errors so exported function behaves as expected.