diff --git a/crates/yoop-core/src/preview/mod.rs b/crates/yoop-core/src/preview/mod.rs index 6299b2d..5a70796 100644 --- a/crates/yoop-core/src/preview/mod.rs +++ b/crates/yoop-core/src/preview/mod.rs @@ -176,10 +176,7 @@ impl PreviewGenerator { let metadata = std::fs::metadata(path)?; - // Try to open the image let Ok(img) = image::open(path) else { - // If image cannot be opened (e.g., unsupported format like JPEG), - // return a preview with just metadata return Ok(Preview { preview_type: PreviewType::Icon, data: String::new(), @@ -194,17 +191,14 @@ impl PreviewGenerator { let (width, height) = img.dimensions(); let (max_w, max_h) = self.config.thumbnail_size; - // Generate thumbnail let thumb = img.thumbnail(max_w, max_h); - // Encode as PNG to a buffer let mut buf = Vec::new(); let mut cursor = Cursor::new(&mut buf); thumb .write_to(&mut cursor, image::ImageFormat::Png) .map_err(|e| crate::error::Error::Io(std::io::Error::other(e.to_string())))?; - // Base64 encode the thumbnail let encoded = base64::engine::general_purpose::STANDARD.encode(&buf); Ok(Preview { @@ -247,7 +241,6 @@ impl PreviewGenerator { #[cfg(not(feature = "web"))] { - // Without the web feature, zip crate is not available let metadata = std::fs::metadata(path)?; Ok(Preview { preview_type: PreviewType::ArchiveListing, @@ -269,9 +262,7 @@ impl PreviewGenerator { let metadata = std::fs::metadata(path)?; let file = File::open(path)?; - // Try to open as ZIP archive let Ok(archive) = zip::ZipArchive::new(file) else { - // Not a valid ZIP file, return empty listing return Ok(Preview { preview_type: PreviewType::ArchiveListing, data: "[]".to_string(), @@ -286,7 +277,6 @@ impl PreviewGenerator { let total_files = archive.len(); - // Collect file names (up to 50 entries) let entries: Vec = archive.file_names().take(50).map(String::from).collect(); let data = serde_json::to_string(&entries) @@ -338,7 +328,6 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let file_path = temp_dir.path().join("large.txt"); - // Create a file larger than max_text_length let content = "x".repeat(2000); std::fs::write(&file_path, &content).unwrap(); @@ -359,7 +348,6 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let file_path = temp_dir.path().join("test.png"); - // Create a simple 2x2 PNG image let img = image::RgbImage::from_fn(100, 100, |x, y| { if (x + y) % 2 == 0 { image::Rgb([255, 0, 0]) @@ -379,7 +367,6 @@ mod tests { "Thumbnail data should not be empty" ); - // Verify dimensions metadata let meta = preview.metadata.unwrap(); assert_eq!(meta.dimensions, Some((100, 100))); } @@ -388,7 +375,6 @@ mod tests { async fn test_preview_type_detection() { let generator = PreviewGenerator::new(); - // Test image detection assert_eq!( generator.determine_preview_type(Path::new("image.png"), None), PreviewType::Thumbnail @@ -398,7 +384,6 @@ mod tests { PreviewType::Thumbnail ); - // Test text detection assert_eq!( generator.determine_preview_type(Path::new("file.txt"), None), PreviewType::Text @@ -412,7 +397,6 @@ mod tests { PreviewType::Text ); - // Test archive detection assert_eq!( generator.determine_preview_type(Path::new("archive.zip"), None), PreviewType::ArchiveListing @@ -422,7 +406,6 @@ mod tests { PreviewType::ArchiveListing ); - // Test icon fallback assert_eq!( generator.determine_preview_type(Path::new("unknown.xyz"), None), PreviewType::Icon @@ -451,7 +434,6 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let zip_path = temp_dir.path().join("test.zip"); - // Create a simple ZIP file with some entries let file = std::fs::File::create(&zip_path).unwrap(); let mut zip = zip::ZipWriter::new(file); diff --git a/crates/yoop-core/src/transfer/mod.rs b/crates/yoop-core/src/transfer/mod.rs index e00a36d..41e21ea 100644 --- a/crates/yoop-core/src/transfer/mod.rs +++ b/crates/yoop-core/src/transfer/mod.rs @@ -263,11 +263,9 @@ impl ShareSession { return Err(Error::FileNotFound("no files to share".to_string())); } - // Generate previews for files let preview_generator = crate::preview::PreviewGenerator::new(); for file in &mut files { if !file.is_directory { - // Find the absolute path for this file for base_path in paths { let absolute_path = if base_path.is_dir() { base_path.join(&file.relative_path) @@ -557,6 +555,23 @@ impl ShareSession { }; let start_payload = protocol::encode_payload(&start)?; protocol::write_frame(stream, MessageType::ChunkStart, &start_payload).await?; + + let (header, ack_payload) = protocol::read_frame(stream).await?; + if header.message_type != MessageType::ChunkAck { + return Err(Error::UnexpectedMessage { + expected: "ChunkAck".to_string(), + actual: format!("{:?}", header.message_type), + }); + } + + let ack: ChunkAckPayload = protocol::decode_payload(&ack_payload)?; + if !ack.success { + return Err(Error::ProtocolError(format!( + "Receiver failed to create directory: {}", + file.file_name() + ))); + } + tracing::debug!( "Sent directory marker for file {}: {}", file_index, @@ -578,11 +593,29 @@ impl ShareSession { }; let start_payload = protocol::encode_payload(&start)?; protocol::write_frame(stream, MessageType::ChunkStart, &start_payload).await?; + + let (header, ack_payload) = protocol::read_frame(stream).await?; + if header.message_type != MessageType::ChunkAck { + return Err(Error::UnexpectedMessage { + expected: "ChunkAck".to_string(), + actual: format!("{:?}", header.message_type), + }); + } + + let ack: ChunkAckPayload = protocol::decode_payload(&ack_payload)?; + if !ack.success { + return Err(Error::ProtocolError(format!( + "Receiver failed to create empty file: {}", + file.file_name() + ))); + } + tracing::debug!( "Sent empty file marker for file {}: {}", file_index, file.file_name() ); + continue; } for chunk in chunks { @@ -1431,12 +1464,16 @@ impl ReceiveSession { Ok(file_list.files) } - async fn handle_chunk_start( + async fn handle_chunk_start( &self, + stream: &mut S, start: ChunkStartPayload, current_writer: &mut Option, current_file_index: &mut Option, - ) -> Result<()> { + ) -> Result<()> + where + S: AsyncRead + AsyncWrite + Unpin, + { if *current_file_index != Some(start.file_index) { if let Some(writer) = current_writer.take() { let _sha256 = writer.finalize().await?; @@ -1445,7 +1482,7 @@ impl ReceiveSession { let file = &self.files[start.file_index]; let output_path = self.output_dir.join(&file.relative_path); - if start.total_chunks == 0 || file.is_directory { + if file.is_directory { tokio::fs::create_dir_all(&output_path).await.map_err(|e| { Error::Io(std::io::Error::new( e.kind(), @@ -1471,6 +1508,67 @@ impl ReceiveSession { } tracing::debug!("Created directory: {}", output_path.display()); + + let ack = ChunkAckPayload { + file_index: start.file_index, + chunk_index: 0, + success: true, + }; + let ack_payload = protocol::encode_payload(&ack)?; + protocol::write_frame(stream, MessageType::ChunkAck, &ack_payload).await?; + + *current_file_index = Some(start.file_index); + return Ok(()); + } + + if start.total_chunks == 0 { + if let Some(parent) = output_path.parent() { + tokio::fs::create_dir_all(parent).await.map_err(|e| { + Error::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to create parent directory {}: {}", + parent.display(), + e + ), + )) + })?; + } + + tokio::fs::File::create(&output_path).await.map_err(|e| { + Error::Io(std::io::Error::new( + e.kind(), + format!( + "Failed to create empty file {}: {}", + output_path.display(), + e + ), + )) + })?; + + #[cfg(unix)] + if let Some(mode) = file.permissions { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(mode); + if let Err(e) = std::fs::set_permissions(&output_path, perms) { + tracing::warn!( + "Failed to set permissions on file {}: {}", + output_path.display(), + e + ); + } + } + + tracing::debug!("Created empty file: {}", output_path.display()); + + let ack = ChunkAckPayload { + file_index: start.file_index, + chunk_index: 0, + success: true, + }; + let ack_payload = protocol::encode_payload(&ack)?; + protocol::write_frame(stream, MessageType::ChunkAck, &ack_payload).await?; + *current_file_index = Some(start.file_index); return Ok(()); } @@ -1559,8 +1657,13 @@ impl ReceiveSession { match header.message_type { MessageType::ChunkStart => { let start: ChunkStartPayload = protocol::decode_payload(&payload)?; - self.handle_chunk_start(start, &mut current_writer, &mut current_file_index) - .await?; + self.handle_chunk_start( + stream, + start, + &mut current_writer, + &mut current_file_index, + ) + .await?; } MessageType::ChunkData => { self.handle_chunk_data(stream, &payload, &mut current_writer) diff --git a/npm/yoop/bin.js b/npm/yoop/bin.js index 50c6211..4d48402 100644 --- a/npm/yoop/bin.js +++ b/npm/yoop/bin.js @@ -4,92 +4,92 @@ const { spawn } = require("child_process"); const path = require("path"); const fs = require("fs"); -/** - * Platform to npm package name mapping - */ const PLATFORMS = { - "linux-x64": { - packages: ["@sanchxt/yoop-linux-x64-musl", "@sanchxt/yoop-linux-x64-gnu"], - }, - "linux-arm64": { - packages: ["@sanchxt/yoop-linux-arm64-gnu"], - }, - "darwin-x64": { - packages: ["@sanchxt/yoop-darwin-x64"], - }, - "darwin-arm64": { - packages: ["@sanchxt/yoop-darwin-arm64"], - }, - "win32-x64": { - packages: ["@sanchxt/yoop-win32-x64-msvc"], - }, + "linux-x64": { + packages: [ + "@sanchxt/yoop-linux-x64-musl", + "@sanchxt/yoop-linux-x64-gnu", + ], + }, + "linux-arm64": { + packages: ["@sanchxt/yoop-linux-arm64-gnu"], + }, + "darwin-x64": { + packages: ["@sanchxt/yoop-darwin-x64"], + }, + "darwin-arm64": { + packages: ["@sanchxt/yoop-darwin-arm64"], + }, + "win32-x64": { + packages: ["@sanchxt/yoop-win32-x64-msvc"], + }, }; -/** - * Find the binary path for the current platform - */ function getBinaryPath() { - const platform = process.platform; - const arch = process.arch; - const key = `${platform}-${arch}`; + const platform = process.platform; + const arch = process.arch; + const key = `${platform}-${arch}`; - const platformConfig = PLATFORMS[key]; - if (!platformConfig) { - console.error(`Error: Unsupported platform: ${platform}-${arch}`); - console.error("Supported platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64"); - process.exit(1); - } + const platformConfig = PLATFORMS[key]; + if (!platformConfig) { + console.error(`Error: Unsupported platform: ${platform}-${arch}`); + console.error( + "Supported platforms: linux-x64, linux-arm64, darwin-x64, darwin-arm64, win32-x64" + ); + process.exit(1); + } - const binName = platform === "win32" ? "yoop.exe" : "yoop"; + const binName = platform === "win32" ? "yoop.exe" : "yoop"; - // Try each package in order (musl first for Linux, as it's more portable) - for (const packageName of platformConfig.packages) { - try { - const packagePath = require.resolve(`${packageName}/package.json`); - const binPath = path.join(path.dirname(packagePath), "bin", binName); + for (const packageName of platformConfig.packages) { + try { + const packagePath = require.resolve(`${packageName}/package.json`); + const binPath = path.join( + path.dirname(packagePath), + "bin", + binName + ); - if (fs.existsSync(binPath)) { - return binPath; - } - } catch { - // Package not installed, try next - continue; + if (fs.existsSync(binPath)) { + return binPath; + } + } catch { + continue; + } } - } - console.error(`Error: Could not find yoop binary for ${platform}-${arch}`); - console.error(""); - console.error("This usually means the platform-specific package failed to install."); - console.error("Try reinstalling:"); - console.error(" npm install -g yoop"); - console.error(""); - console.error("If the problem persists, please file an issue at:"); - console.error(" https://github.com/sanchxt/yoop/issues"); - process.exit(1); + console.error(`Error: Could not find yoop binary for ${platform}-${arch}`); + console.error(""); + console.error( + "This usually means the platform-specific package failed to install." + ); + console.error("Try reinstalling:"); + console.error(" npm install -g yoop"); + console.error(""); + console.error("If the problem persists, please file an issue at:"); + console.error(" https://github.com/sanchxt/yoop/issues"); + process.exit(1); } -/** - * Run the binary with the given arguments - */ function run() { - const binPath = getBinaryPath(); + const binPath = getBinaryPath(); - const child = spawn(binPath, process.argv.slice(2), { - stdio: "inherit", - shell: false, - }); + const child = spawn(binPath, process.argv.slice(2), { + stdio: "inherit", + shell: false, + }); - child.on("error", (err) => { - console.error(`Error: Failed to execute yoop: ${err.message}`); - process.exit(1); - }); + child.on("error", (err) => { + console.error(`Error: Failed to execute yoop: ${err.message}`); + process.exit(1); + }); - child.on("exit", (code, signal) => { - if (signal) { - process.exit(1); - } - process.exit(code ?? 0); - }); + child.on("exit", (code, signal) => { + if (signal) { + process.exit(1); + } + process.exit(code ?? 0); + }); } run(); diff --git a/scripts/sync-versions.js b/scripts/sync-versions.js index a682b24..7e2077b 100755 --- a/scripts/sync-versions.js +++ b/scripts/sync-versions.js @@ -18,138 +18,123 @@ const ROOT_DIR = path.join(__dirname, ".."); const NPM_DIR = path.join(ROOT_DIR, "npm"); const CARGO_TOML = path.join(ROOT_DIR, "Cargo.toml"); -/** - * Extract version from Cargo.toml - */ function getCargoVersion() { - const content = fs.readFileSync(CARGO_TOML, "utf8"); - - // Look for version in [workspace.package] section - const workspaceMatch = content.match( - /\[workspace\.package\][\s\S]*?version\s*=\s*"([^"]+)"/ - ); - if (workspaceMatch) { - return workspaceMatch[1]; - } - - // Fallback: look for version at top level - const topMatch = content.match(/^version\s*=\s*"([^"]+)"/m); - if (topMatch) { - return topMatch[1]; - } - - throw new Error("Could not find version in Cargo.toml"); + const content = fs.readFileSync(CARGO_TOML, "utf8"); + + const workspaceMatch = content.match( + /\[workspace\.package\][\s\S]*?version\s*=\s*"([^"]+)"/ + ); + if (workspaceMatch) { + return workspaceMatch[1]; + } + + const topMatch = content.match(/^version\s*=\s*"([^"]+)"/m); + if (topMatch) { + return topMatch[1]; + } + + throw new Error("Could not find version in Cargo.toml"); } -/** - * Find all package.json files in npm directory - */ function findPackageJsonFiles() { - const files = []; - - function walk(dir) { - const entries = fs.readdirSync(dir, { withFileTypes: true }); - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory()) { - // Skip node_modules and bin directories - if (entry.name !== "node_modules" && entry.name !== "bin") { - walk(fullPath); + const files = []; + + function walk(dir) { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name !== "node_modules" && entry.name !== "bin") { + walk(fullPath); + } + } else if (entry.name === "package.json") { + files.push(fullPath); + } } - } else if (entry.name === "package.json") { - files.push(fullPath); - } } - } - if (fs.existsSync(NPM_DIR)) { - walk(NPM_DIR); - } + if (fs.existsSync(NPM_DIR)) { + walk(NPM_DIR); + } - return files; + return files; } -/** - * Update version in a package.json file - */ function updatePackageJson(filePath, newVersion, checkOnly) { - const content = fs.readFileSync(filePath, "utf8"); - const pkg = JSON.parse(content); - const oldVersion = pkg.version; - let changed = false; - - // Update main version - if (pkg.version !== newVersion) { - pkg.version = newVersion; - changed = true; - } - - // Update optionalDependencies versions (for main package) - if (pkg.optionalDependencies) { - for (const dep of Object.keys(pkg.optionalDependencies)) { - if (dep.startsWith("@sanchxt/yoop-")) { - if (pkg.optionalDependencies[dep] !== newVersion) { - pkg.optionalDependencies[dep] = newVersion; - changed = true; + const content = fs.readFileSync(filePath, "utf8"); + const pkg = JSON.parse(content); + const oldVersion = pkg.version; + let changed = false; + + if (pkg.version !== newVersion) { + pkg.version = newVersion; + changed = true; + } + + if (pkg.optionalDependencies) { + for (const dep of Object.keys(pkg.optionalDependencies)) { + if (dep.startsWith("@sanchxt/yoop-")) { + if (pkg.optionalDependencies[dep] !== newVersion) { + pkg.optionalDependencies[dep] = newVersion; + changed = true; + } + } } - } } - } - if (changed) { - const relativePath = path.relative(ROOT_DIR, filePath); - if (checkOnly) { - console.log(` ${relativePath}: ${oldVersion} -> ${newVersion} (needs update)`); - } else { - fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + "\n"); - console.log(` ${relativePath}: ${oldVersion} -> ${newVersion}`); + if (changed) { + const relativePath = path.relative(ROOT_DIR, filePath); + if (checkOnly) { + console.log( + ` ${relativePath}: ${oldVersion} -> ${newVersion} (needs update)` + ); + } else { + fs.writeFileSync(filePath, JSON.stringify(pkg, null, 2) + "\n"); + console.log(` ${relativePath}: ${oldVersion} -> ${newVersion}`); + } } - } - return changed; + return changed; } -/** - * Main entry point - */ function main() { - const args = process.argv.slice(2); - const checkOnly = args.includes("--check"); - - console.log("Syncing npm package versions with Cargo.toml...\n"); - - // Get version from Cargo.toml - const version = getCargoVersion(); - console.log(`Cargo.toml version: ${version}\n`); - - // Find and update all package.json files - const packageFiles = findPackageJsonFiles(); - if (packageFiles.length === 0) { - console.log("No package.json files found in npm/ directory."); - process.exit(0); - } - - console.log( - checkOnly ? "Checking package.json files:" : "Updating package.json files:" - ); - - let updatedCount = 0; - for (const file of packageFiles) { - if (updatePackageJson(file, version, checkOnly)) { - updatedCount++; + const args = process.argv.slice(2); + const checkOnly = args.includes("--check"); + + console.log("Syncing npm package versions with Cargo.toml...\n"); + + const version = getCargoVersion(); + console.log(`Cargo.toml version: ${version}\n`); + + const packageFiles = findPackageJsonFiles(); + if (packageFiles.length === 0) { + console.log("No package.json files found in npm/ directory."); + process.exit(0); + } + + console.log( + checkOnly + ? "Checking package.json files:" + : "Updating package.json files:" + ); + + let updatedCount = 0; + for (const file of packageFiles) { + if (updatePackageJson(file, version, checkOnly)) { + updatedCount++; + } + } + + console.log(""); + + if (updatedCount === 0) { + console.log("All package.json files are already up to date."); + } else if (checkOnly) { + console.log(`${updatedCount} file(s) need updating.`); + process.exit(1); + } else { + console.log(`Updated ${updatedCount} file(s).`); } - } - - console.log(""); - - if (updatedCount === 0) { - console.log("All package.json files are already up to date."); - } else if (checkOnly) { - console.log(`${updatedCount} file(s) need updating.`); - process.exit(1); - } else { - console.log(`Updated ${updatedCount} file(s).`); - } } main();