Skip to content

Commit 3e21ea6

Browse files
committed
fix: avoid empty files
1 parent 58a6e44 commit 3e21ea6

File tree

5 files changed

+106
-121
lines changed

5 files changed

+106
-121
lines changed

Cargo.toml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,11 @@ version = "0.0.0"
77
crate-type = ["cdylib"]
88

99
[dependencies]
10+
futures = "0.3.28"
1011
# Default enable napi4 feature, see https://nodejs.org/api/n-api.html#node-api-version-matrix
1112
napi = { version = "2.12.2", default-features = false, features = ["napi4"] }
1213
napi-derive = "2.12.2"
13-
reflink-copy = "0.1.9"
14-
15-
[target.'cfg(unix)'.dependencies]
16-
xattr = "1.0.1"
14+
reflink-copy = { git = "https://github.com/nachoaldamav/reflink-copy", rev = "a55b1e9" }
1715

1816
[build-dependencies]
1917
napi-build = "2.0.1"

benchmark.mjs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { join } from 'path';
2+
import { mkdir, rm, writeFile, link } from 'fs/promises';
3+
import { linkSync } from 'fs';
4+
import { randomUUID } from 'crypto';
5+
import { performance } from 'perf_hooks';
6+
import chalk from 'chalk';
7+
import { reflinkFileSync, reflinkFile } from './index.js';
8+
9+
const sandboxDir = join(process.cwd(), `__link-tests-${randomUUID()}`);
10+
const testFilePath = join(sandboxDir, 'testFile.txt');
11+
12+
const results = {};
13+
14+
async function setup() {
15+
await rm(sandboxDir, { recursive: true, force: true });
16+
await mkdir(sandboxDir, { recursive: true });
17+
await writeFile(testFilePath, 'Hello, world!');
18+
}
19+
20+
async function teardown() {
21+
await rm(sandboxDir, { recursive: true, force: true });
22+
}
23+
24+
async function runBenchmark(name, fn) {
25+
await setup();
26+
const start = performance.now();
27+
for (let i = 0; i < 1000; i++) {
28+
const destPath = join(sandboxDir, `clone-${i}.txt`);
29+
await fn(destPath);
30+
}
31+
const end = performance.now();
32+
const time = end - start;
33+
results[name] = time.toFixed(2);
34+
console.log(chalk.green(`${name}: ${chalk.blue(time)} ms`));
35+
await teardown();
36+
}
37+
38+
function delay(ms) {
39+
return new Promise((resolve) => setTimeout(resolve, ms));
40+
}
41+
42+
async function main() {
43+
console.log(chalk.bold('Running Benchmarks...'));
44+
45+
await runBenchmark('Node fs.linkSync', (destPath) => {
46+
linkSync(testFilePath, destPath);
47+
});
48+
await delay(2000);
49+
50+
await runBenchmark('Node fs.promises.link', async (destPath) => {
51+
await link(testFilePath, destPath);
52+
});
53+
await delay(2000);
54+
55+
await runBenchmark('reflinkFileSync', (destPath) => {
56+
reflinkFileSync(testFilePath, destPath);
57+
});
58+
await delay(2000);
59+
60+
await runBenchmark('reflinkFile', async (destPath) => {
61+
await reflinkFile(testFilePath, destPath);
62+
});
63+
64+
console.log(chalk.bold('\nBenchmark Summary:'));
65+
for (const [name, time] of Object.entries(results)) {
66+
console.log(`${name}: ${time} ms`);
67+
}
68+
69+
const fastest = Object.entries(results).sort((a, b) => a[1] - b[1])[0];
70+
console.log(
71+
chalk.green.bold(`\nFastest is ${fastest[0]} with ${fastest[1]} ms`)
72+
);
73+
}
74+
75+
main().catch((err) => {
76+
console.error(chalk.red('An error occurred:', err));
77+
process.exit(1);
78+
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"prepublishOnly": "napi prepublish -t npm",
4343
"pretest": "yarn build",
4444
"test": "cargo t && vitest",
45+
"bench": "node benchmark.mjs",
4546
"universal": "napi universal",
4647
"version": "napi version"
4748
},

src/lib.rs

Lines changed: 24 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -6,78 +6,28 @@ extern crate napi_derive;
66
use napi::{bindgen_prelude::AsyncTask, Env, Error, JsNumber, Result, Task};
77
use std::path::PathBuf;
88
use reflink_copy;
9-
use std::fs;
10-
11-
#[cfg(not(target_os = "windows"))]
12-
extern crate xattr;
139

1410
pub struct AsyncReflink {
1511
src: PathBuf,
1612
dst: PathBuf,
1713
}
1814

19-
#[cfg(not(target_os = "windows"))]
20-
fn set_destination_metadata(src: &PathBuf, dst: &PathBuf) -> std::io::Result<()> {
21-
let metadata_key = "user.reflink_destinations";
22-
23-
let mut destinations = match xattr::get(src, metadata_key) {
24-
Ok(Some(data)) => String::from_utf8_lossy(&data).to_string(),
25-
_ => String::from(""),
26-
};
27-
28-
if !destinations.is_empty() {
29-
destinations.push_str(",");
30-
}
31-
destinations.push_str(dst.to_str().unwrap());
32-
33-
xattr::set(src, metadata_key, destinations.as_bytes())
34-
}
35-
3615
#[napi]
3716
impl Task for AsyncReflink {
3817
type Output = ();
3918
type JsValue = JsNumber;
4019

4120
fn compute(&mut self) -> Result<Self::Output> {
42-
let mut retry_count = 0;
43-
loop {
44-
match reflink_copy::reflink(&self.src, &self.dst) {
45-
Ok(_) => {
46-
#[cfg(not(target_os = "windows"))]
47-
{
48-
if let Err(err) = set_destination_metadata(&self.src, &self.dst) {
49-
return Err(Error::from_reason(err.to_string()));
50-
}
51-
}
52-
53-
// Further validation: compare the contents of both files to make sure they are identical
54-
let src_contents = fs::read(&self.src).map_err(|e| Error::from_reason(e.to_string()))?;
55-
let dst_contents = fs::read(&self.dst).map_err(|e| Error::from_reason(e.to_string()))?;
56-
57-
if src_contents != dst_contents {
58-
// Delete the destination and retry if the files are not identical
59-
fs::remove_file(&self.dst).map_err(|e| Error::from_reason(e.to_string()))?;
60-
retry_count += 1;
61-
62-
if retry_count >= 3 { // Limit the number of retries
63-
return Err(Error::from_reason(format!(
64-
"Max retries reached, could not create identical reflink for '{}' -> '{}'",
65-
self.src.display(),
66-
self.dst.display()
67-
)));
68-
}
69-
continue; // Retry the operation
70-
}
71-
72-
return Ok(());
73-
},
74-
Err(err) => return Err(Error::from_reason(format!(
75-
"{}, reflink '{}' -> '{}'",
76-
err.to_string(),
77-
self.src.display(),
78-
self.dst.display()
79-
))),
80-
}
21+
match reflink_copy::reflink(&self.src, &self.dst) {
22+
Ok(_) => {
23+
Ok(())
24+
},
25+
Err(err) => return Err(Error::from_reason(format!(
26+
"{}, reflink '{}' -> '{}'",
27+
err.to_string(),
28+
self.src.display(),
29+
self.dst.display()
30+
))),
8131
}
8232
}
8333

@@ -86,7 +36,6 @@ impl Task for AsyncReflink {
8636
}
8737
}
8838

89-
9039
// Async version
9140
#[napi(js_name = "reflinkFile")]
9241
pub fn reflink_task(src: String, dst: String) -> AsyncTask<AsyncReflink> {
@@ -98,58 +47,16 @@ pub fn reflink_task(src: String, dst: String) -> AsyncTask<AsyncReflink> {
9847
// Sync version
9948
#[napi(js_name = "reflinkFileSync")]
10049
pub fn reflink_sync(env: Env, src: String, dst: String) -> Result<JsNumber> {
101-
let src_path = PathBuf::from(src.clone());
102-
let dst_path = PathBuf::from(dst.clone());
103-
let mut retry_count = 0;
104-
105-
loop {
106-
// Attempt to perform reflink
107-
let reflink_result = reflink_copy::reflink(&src_path, &dst_path);
108-
109-
match reflink_result {
110-
Ok(_) => {
111-
// Further validation: compare the contents of both files to make sure they are identical
112-
let src_contents = fs::read(&src_path).map_err(|e| Error::from_reason(e.to_string()))?;
113-
let dst_contents = fs::read(&dst_path).map_err(|e| Error::from_reason(e.to_string()))?;
114-
115-
if src_contents != dst_contents {
116-
if retry_count >= 3 { // Max retry count
117-
return Err(Error::from_reason(format!(
118-
"Max retries reached, could not create identical reflink for '{}' -> '{}'",
119-
src_path.display(),
120-
dst_path.display()
121-
)));
122-
}
123-
// Remove the destination and retry
124-
if let Err(err) = fs::remove_file(&dst_path) {
125-
return Err(Error::from_reason(format!(
126-
"Failed to remove destination file '{}': {}",
127-
dst_path.display(),
128-
err.to_string()
129-
)));
130-
}
131-
retry_count += 1;
132-
continue; // Retry the operation
133-
}
134-
135-
// Metadata and return handling here (existing code)
136-
#[cfg(not(target_os = "windows"))]
137-
{
138-
if let Err(err) = set_destination_metadata(&src_path, &dst_path) {
139-
return Err(Error::from_reason(err.to_string()));
140-
}
141-
}
142-
return Ok(env.create_int32(0)?);
143-
},
144-
Err(err) => {
145-
return Err(Error::from_reason(format!(
146-
"{}, reflink '{}' -> '{}'",
147-
err.to_string(),
148-
src_path.display(),
149-
dst_path.display()
150-
)));
151-
},
152-
}
50+
let src_path = PathBuf::from(src);
51+
let dst_path = PathBuf::from(dst);
52+
match reflink_copy::reflink(&src_path, &dst_path) {
53+
Ok(_) => Ok(env.create_int32(0)?),
54+
Err(err) => Err(Error::from_reason(format!(
55+
"{}, reflink '{}' -> '{}'",
56+
err.to_string(),
57+
src_path.display(),
58+
dst_path.display()
59+
))),
15360
}
15461
}
15562

@@ -160,7 +67,7 @@ pub fn test_pyc_file() {
16067

16168
// Remove the destination file if it already exists
16269
if dst.exists() {
163-
fs::remove_file(&dst).unwrap();
70+
std::fs::remove_file(&dst).unwrap();
16471
}
16572

16673
// Run the reflink operation
@@ -170,13 +77,13 @@ pub fn test_pyc_file() {
17077
println!("Reflinked '{}' -> '{}'", src.display(), dst.display());
17178

17279
// Further validation: compare the contents of both files to make sure they are identical
173-
let src_contents = fs::read(&src).expect("Failed to read source file");
174-
let dst_contents = fs::read(&dst).expect("Failed to read destination file");
80+
let src_contents = std::fs::read(&src).expect("Failed to read source file");
81+
let dst_contents = std::fs::read(&dst).expect("Failed to read destination file");
17582

17683
assert_eq!(src_contents, dst_contents);
17784

17885
// Remove the destination file
179-
fs::remove_file(&dst).unwrap();
86+
std::fs::remove_file(&dst).unwrap();
18087

18188
println!("File contents match, reflink operation successful")
18289
}

vitest.config.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,6 @@ export default defineConfig({
44
test: {
55
watchExclude: ['__reflink-tests-*'],
66
watch: false,
7+
testTimeout: 10000,
78
},
89
});

0 commit comments

Comments
 (0)