diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..b3fc4706 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=crlf \ No newline at end of file diff --git a/.github/workflows/check-pr-size.yml b/.github/workflows/check-pr-size.yml new file mode 100644 index 00000000..6289183a --- /dev/null +++ b/.github/workflows/check-pr-size.yml @@ -0,0 +1,46 @@ +name: Check PR size + +on: + pull_request: + +jobs: + check_pr_size: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - name: Calculate changed lines + id: diff_check + run: | + # Get the target branch commit (base) and the PR branch commit (head) + BASE_SHA="${{ github.event.pull_request.base.sha }}" + HEAD_SHA="${{ github.event.pull_request.head.sha }}" + echo "Base SHA: $BASE_SHA" + echo "Head SHA: $HEAD_SHA" + + # Compute the merge base between the two branches + MERGE_BASE=$(git merge-base "$HEAD_SHA" "$BASE_SHA") + echo "Merge Base: $MERGE_BASE" + + # Calculate added and deleted lines between the merge base and the head commit + TOTAL_CHANGED=$(git diff --numstat "$MERGE_BASE" "$HEAD_SHA" \ + | grep -v "yarn.lock" \ + | awk '{ added += $1; deleted += $2 } END { print added + deleted }') + + # Default to 0 if nothing is output + TOTAL_CHANGED=${TOTAL_CHANGED:-0} + echo "Total changed lines: $TOTAL_CHANGED" + + # Make the total available for later steps + echo "total=$TOTAL_CHANGED" >> "$GITHUB_OUTPUT" + + - name: Fail if too many changes + if: ${{ steps.diff_check.outputs.total > 500 }} + run: | + echo "PR has ${{ steps.diff_check.outputs.total }} changed lines, which exceeds the 500-line limit." + echo "Please reduce the size of this PR." + exit 1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a62f77f4..b8f9ad64 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,7 @@ on: release: types: [created] workflow_dispatch: - + jobs: build-windows: runs-on: windows-latest @@ -12,7 +12,7 @@ jobs: packages: write steps: - uses: actions/checkout@v2 - - run: yarn build:gyp + - run: yarn build:gyp - uses: actions/upload-artifact@v2 with: name: windows-binaries @@ -32,11 +32,11 @@ jobs: path: build/Release - uses: actions/setup-node@v2 with: - node-version: '16.x' - registry-url: 'https://npm.pkg.github.com' + node-version: "16.x" + registry-url: "https://npm.pkg.github.com" - run: yarn env: - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: yarn publish env: NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 988992fd..7e9a6ad6 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -1,11 +1,11 @@ -name: Some pull request checks +name: Pull request checks on: pull_request: jobs: - type-check: - runs-on: ubuntu-latest + checks: + runs-on: windows-latest steps: - name: Checkout code @@ -22,5 +22,8 @@ jobs: - name: Run TypeScript compiler run: yarn tsc - # - name: Run Vitest - # run: yarn vitest:once + - name: Run Prettier + run: yarn prettier . --check + + - name: Run tests + run: yarn test:once diff --git a/.github/workflows/sonar-analysis.yml b/.github/workflows/sonar-analysis.yml new file mode 100644 index 00000000..c54798f0 --- /dev/null +++ b/.github/workflows/sonar-analysis.yml @@ -0,0 +1,47 @@ +name: SonarCloud code analysis + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + runs-on: windows-latest + + env: + BUILD_WRAPPER_OUT_DIR: build_wrapper_output_directory + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 18 + + - name: Install node-gyp + run: npm install -g node-gyp + + - name: Install Build Wrapper + uses: SonarSource/sonarqube-scan-action/install-build-wrapper@v5 + + - name: Run Build Wrapper + run: build-wrapper-win-x86-64.exe --out-dir ${{ env.BUILD_WRAPPER_OUT_DIR }} node-gyp configure build + + - run: ls ${{ env.BUILD_WRAPPER_OUT_DIR }} + - run: cat ${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json + - run: cat ${{ env.BUILD_WRAPPER_OUT_DIR }}/build-wrapper.log + - run: cat ${{ env.BUILD_WRAPPER_OUT_DIR }}/build-wrapper-dump.json + + - name: SonarQube Scan + uses: SonarSource/sonarqube-scan-action@v5 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + with: + args: > + --define sonar.cfamily.compile-commands="${{ env.BUILD_WRAPPER_OUT_DIR }}/compile_commands.json" + + diff --git a/.gitignore b/.gitignore index bc299f21..f866b1f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ /.env /.vscode /addon.node +/bin /build /dist /examples/tmp diff --git a/examples/drive.ts b/examples/drive.ts index 3322737f..5d47137e 100644 --- a/examples/drive.ts +++ b/examples/drive.ts @@ -3,5 +3,5 @@ import VirtualDrive from "@/virtual-drive"; import settings from "./settings"; -export const drive = new VirtualDrive(settings.syncRootPath, settings.defaultLogPath); +export const drive = new VirtualDrive(settings.syncRootPath, settings.providerid, settings.defaultLogPath); export const logger = createLogger(settings.defaultLogPath); diff --git a/examples/populate.ts b/examples/populate.ts index 0843a9d7..99cc31f1 100644 --- a/examples/populate.ts +++ b/examples/populate.ts @@ -1,9 +1,9 @@ -import VirtualDrive from "@/virtual-drive"; - import { execSync } from "child_process"; import { join } from "path"; import { v4 } from "uuid"; +import VirtualDrive from "@/virtual-drive"; + import settings from "./settings"; const rootFileName1 = v4(); diff --git a/examples/register.ts b/examples/register.ts index 5ed00fbe..108bff7d 100644 --- a/examples/register.ts +++ b/examples/register.ts @@ -20,7 +20,7 @@ const handlers = { handleAdd, handleHydrate, handleDehydrate, handleChangeSize } const notify = { onTaskSuccess: async () => undefined, onTaskProcessing: async () => undefined }; const queueManager = new QueueManager(handlers, notify, settings.queuePersistPath); -drive.registerSyncRoot(settings.driveName, settings.driveVersion, settings.providerid, callbacks, settings.iconPath); +drive.registerSyncRoot(settings.driveName, settings.driveVersion, callbacks, settings.iconPath); drive.connectSyncRoot(); try { @@ -29,5 +29,5 @@ try { } catch (error) { logger.error(error); drive.disconnectSyncRoot(); - VirtualDrive.unregisterSyncRoot(settings.syncRootPath); + drive.unregisterSyncRoot(); } diff --git a/examples/unregister.ts b/examples/unregister.ts index 6c57ca89..b5446fcd 100644 --- a/examples/unregister.ts +++ b/examples/unregister.ts @@ -1,7 +1,7 @@ import VirtualDrive from "@/virtual-drive"; +import { drive } from "./drive"; import { deleteInfoItems } from "./info-items-manager"; -import settings from "./settings"; -VirtualDrive.unregisterSyncRoot(settings.syncRootPath); +drive.unregisterSyncRoot(); deleteInfoItems(); diff --git a/examples/utils.ts b/examples/utils.ts index 5636667d..fc0dd972 100644 --- a/examples/utils.ts +++ b/examples/utils.ts @@ -1,47 +1,46 @@ -import fs from 'fs'; -import path from 'path'; +import fs from "fs"; +import path from "path"; interface FileDetail { - path: string; - size: number; - baseDir: string; + path: string; + size: number; + baseDir: string; } function readFilesRecursively(dir: string, fileList: FileDetail[] = []): FileDetail[] { - fs.readdirSync(dir).forEach(file => { - const filePath = path.join(dir, file); - if (fs.statSync(filePath).isDirectory()) { - readFilesRecursively(filePath, fileList); - } else { - fileList.push({ - path: filePath, - size: fs.statSync(filePath).size, - baseDir: dir - }); - } - }); - return fileList; + fs.readdirSync(dir).forEach((file) => { + const filePath = path.join(dir, file); + if (fs.statSync(filePath).isDirectory()) { + readFilesRecursively(filePath, fileList); + } else { + fileList.push({ + path: filePath, + size: fs.statSync(filePath).size, + baseDir: dir, + }); + } + }); + return fileList; } function createFilesWithSize(sourceFolder: string, destFolder: string): void { - const files: FileDetail[] = readFilesRecursively(sourceFolder); + const files: FileDetail[] = readFilesRecursively(sourceFolder); - if (!fs.existsSync(destFolder)) { - fs.mkdirSync(destFolder, { recursive: true }); - } - - files.forEach(file => { - const relativePath = path.relative(file.baseDir, file.path); - const destFilePath = path.join(file.baseDir.replace(sourceFolder, destFolder), relativePath);//path.join(destFolder, relativePath); - const destFileDir = file.baseDir.replace(sourceFolder, destFolder);//path.dirname(destFilePath); + if (!fs.existsSync(destFolder)) { + fs.mkdirSync(destFolder, { recursive: true }); + } - if (!fs.existsSync(destFileDir)){ - fs.mkdirSync(destFileDir, { recursive: true }); - } + files.forEach((file) => { + const relativePath = path.relative(file.baseDir, file.path); + const destFilePath = path.join(file.baseDir.replace(sourceFolder, destFolder), relativePath); //path.join(destFolder, relativePath); + const destFileDir = file.baseDir.replace(sourceFolder, destFolder); //path.dirname(destFilePath); - fs.writeFileSync(destFilePath, Buffer.alloc(file.size)); - }); + if (!fs.existsSync(destFileDir)) { + fs.mkdirSync(destFileDir, { recursive: true }); + } + fs.writeFileSync(destFilePath, Buffer.alloc(file.size)); + }); } -export { createFilesWithSize }; \ No newline at end of file +export { createFilesWithSize }; diff --git a/examples/utils/generate-random-file-tree.ts b/examples/utils/generate-random-file-tree.ts index f5cebba8..42b7fc94 100644 --- a/examples/utils/generate-random-file-tree.ts +++ b/examples/utils/generate-random-file-tree.ts @@ -1,8 +1,9 @@ -import VirtualDrive from '@/virtual-drive'; -import { v4 as uuidv4 } from 'uuid'; +import { v4 as uuidv4 } from "uuid"; + +import VirtualDrive from "@/virtual-drive"; interface GenerateOptions { - rootPath: string + rootPath: string; depth: number; filesPerFolder: number; foldersPerLevel: number; @@ -26,18 +27,7 @@ function randomNormal(mean: number, stdDev: number): number { } function getRandomExtension(): string { - const extensions = [ - ".txt", - ".pdf", - ".rar", - ".jpg", - ".docx", - ".xlsx", - ".mp4", - ".mkv", - ".json", - "" - ]; + const extensions = [".txt", ".pdf", ".rar", ".jpg", ".docx", ".xlsx", ".mp4", ".mkv", ".json", ""]; const index = Math.floor(Math.random() * extensions.length); return extensions[index]; } @@ -47,17 +37,11 @@ async function createStructureRecursively( currentPath: string, level: number, options: GenerateOptions, - result: Record + result: Record, ): Promise { if (level < 0) return; - const { - filesPerFolder, - foldersPerLevel, - meanSize, - stdDev, - timeOffset - } = options; + const { filesPerFolder, foldersPerLevel, meanSize, stdDev, timeOffset } = options; for (let i = 0; i < filesPerFolder; i++) { const fileName = `file_${generateRandomId()}${getRandomExtension()}`; @@ -69,13 +53,7 @@ async function createStructureRecursively( const createdAt = Date.now() - (timeOffset || 0); const updatedAt = Date.now() - (timeOffset || 0) + 2000; - drive.createFileByPath( - fullPath, - fileId, - fileSize, - createdAt, - updatedAt - ); + drive.createFileByPath(fullPath, fileId, fileSize, createdAt, updatedAt); result[fileId] = fullPath; } @@ -88,22 +66,13 @@ async function createStructureRecursively( const createdAt = Date.now() - (timeOffset || 0) - 10000; // Ejemplo const updatedAt = Date.now() - (timeOffset || 0); - drive.createFolderByPath( - newFolderPath, - folderId, - 1000, - createdAt, - updatedAt - ); + drive.createFolderByPath(newFolderPath, folderId, 1000, createdAt, updatedAt); await createStructureRecursively(drive, newFolderPath, level - 1, options, result); } } -async function generateRandomFilesAndFolders( - drive: VirtualDrive, - options: GenerateOptions -): Promise> { +async function generateRandomFilesAndFolders(drive: VirtualDrive, options: GenerateOptions): Promise> { const { rootPath } = options; const result: Record = {}; @@ -114,4 +83,4 @@ async function generateRandomFilesAndFolders( } export { generateRandomFilesAndFolders }; -export type { GenerateOptions } \ No newline at end of file +export type { GenerateOptions }; diff --git a/gyp.config.json b/gyp.config.json index c62dde82..66c437f2 100644 --- a/gyp.config.json +++ b/gyp.config.json @@ -1,7 +1,7 @@ { - "gyp_file": "binding.gyp", - "source_dirs": ["native-src/**/*.cpp"], - "ignored_source_dirs": [], - "include_dirs": ["include"], - "ignored_include_dirs": [] -} \ No newline at end of file + "gyp_file": "binding.gyp", + "source_dirs": ["native-src/**/*.cpp"], + "ignored_source_dirs": [], + "include_dirs": ["include"], + "ignored_include_dirs": [] +} diff --git a/include/sync_root_interface/SyncRoot.h b/include/sync_root_interface/SyncRoot.h index 25238e3c..6d74f738 100644 --- a/include/sync_root_interface/SyncRoot.h +++ b/include/sync_root_interface/SyncRoot.h @@ -16,11 +16,13 @@ class SyncRoot public: static HRESULT RegisterSyncRoot(const wchar_t *syncRootPath, const wchar_t *providerName, const wchar_t *providerVersion, const GUID &providerId, const wchar_t *logoPath); static HRESULT ConnectSyncRoot(const wchar_t *syncRootPath, InputSyncCallbacks syncCallbacks, napi_env env, CF_CONNECTION_KEY *connectionKey); - static HRESULT DisconnectSyncRoot(); - static HRESULT UnregisterSyncRoot(); - static std::list GetItemsSyncRoot(const wchar_t *syncRootPath); + static HRESULT DisconnectSyncRoot(const wchar_t *syncRootPath); + static HRESULT UnregisterSyncRoot(const GUID &providerId); static std::string GetFileIdentity(const wchar_t *path); static void HydrateFile(const wchar_t *filePath); static void DehydrateFile(const wchar_t *filePath); static void DeleteFileSyncRoot(const wchar_t *path); + +private: + CF_CONNECTION_KEY connectionKey; }; \ No newline at end of file diff --git a/native-src/sync_root_interface/SyncRoot.cpp b/native-src/sync_root_interface/SyncRoot.cpp index 04125741..3cb6c7b6 100644 --- a/native-src/sync_root_interface/SyncRoot.cpp +++ b/native-src/sync_root_interface/SyncRoot.cpp @@ -1,6 +1,6 @@ -#include "stdafx.h" -#include "SyncRoot.h" #include "Callbacks.h" +#include "SyncRoot.h" +#include "stdafx.h" #include #include #include @@ -9,6 +9,7 @@ namespace fs = std::filesystem; // variable to disconect CF_CONNECTION_KEY gloablConnectionKey; +std::map connectionMap; void TransformInputCallbacksToSyncCallbacks(napi_env env, InputSyncCallbacks input) { @@ -115,16 +116,17 @@ HRESULT SyncRoot::RegisterSyncRoot(const wchar_t *syncRootPath, const wchar_t *p { try { - auto syncRootID = providerId; + // Convert GUID to string for syncRootID + wchar_t syncRootID[39]; + StringFromGUID2(providerId, syncRootID, 39); winrt::StorageProviderSyncRootInfo info; - info.Id(L"syncRootID"); + info.Id(syncRootID); auto folder = winrt::StorageFolder::GetFolderFromPathAsync(syncRootPath).get(); info.Path(folder); // The string can be in any form acceptable to SHLoadIndirectString. - info.DisplayNameResource(providerName); std::wstring completeIconResource = std::wstring(logoPath) + L",0"; @@ -135,7 +137,7 @@ HRESULT SyncRoot::RegisterSyncRoot(const wchar_t *syncRootPath, const wchar_t *p info.HydrationPolicyModifier(winrt::StorageProviderHydrationPolicyModifier::None); info.PopulationPolicy(winrt::StorageProviderPopulationPolicy::AlwaysFull); info.InSyncPolicy(winrt::StorageProviderInSyncPolicy::FileCreationTime | winrt::StorageProviderInSyncPolicy::DirectoryCreationTime); - info.Version(L"1.0.0"); + info.Version(providerVersion); info.ShowSiblingsAsGroup(false); info.HardlinkPolicy(winrt::StorageProviderHardlinkPolicy::None); @@ -145,9 +147,8 @@ HRESULT SyncRoot::RegisterSyncRoot(const wchar_t *syncRootPath, const wchar_t *p // Context std::wstring syncRootIdentity(syncRootPath); syncRootIdentity.append(L"->"); - syncRootIdentity.append(L"TestProvider"); + syncRootIdentity.append(providerName); - wchar_t const contextString[] = L"TestProviderContextString"; winrt::IBuffer contextBuffer = winrt::CryptographicBuffer::ConvertStringToBinary(syncRootIdentity.data(), winrt::BinaryStringEncoding::Utf8); info.Context(contextBuffer); @@ -165,20 +166,26 @@ HRESULT SyncRoot::RegisterSyncRoot(const wchar_t *syncRootPath, const wchar_t *p catch (...) { wprintf(L"Could not register the sync root, hr %08x\n", static_cast(winrt::to_hresult())); + return E_FAIL; } } -HRESULT SyncRoot::UnregisterSyncRoot() +HRESULT SyncRoot::UnregisterSyncRoot(const GUID &providerId) { try { + // Convert GUID to string for syncRootID + wchar_t syncRootID[39]; + StringFromGUID2(providerId, syncRootID, 39); + Logger::getInstance().log("Unregistering sync root.", LogLevel::INFO); - winrt::StorageProviderSyncRootManager::Unregister(L"syncRootID"); + winrt::StorageProviderSyncRootManager::Unregister(syncRootID); return S_OK; } catch (...) { wprintf(L"Could not unregister the sync root, hr %08x\n", static_cast(winrt::to_hresult())); + return E_FAIL; } } @@ -205,38 +212,38 @@ HRESULT SyncRoot::ConnectSyncRoot(const wchar_t *syncRootPath, InputSyncCallback CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO | CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH, connectionKey); wprintf(L"Connection key: %llu\n", *connectionKey); - gloablConnectionKey = *connectionKey; + if (SUCCEEDED(hr)) + { + connectionMap[syncRootPath] = *connectionKey; + } return hr; } catch (const std::exception &e) { wprintf(L"Excepción capturada: %hs\n", e.what()); - // Aquí puedes decidir si retornar un código de error específico o mantener el E_FAIL. + return E_FAIL; } catch (...) { wprintf(L"Excepción desconocida capturada\n"); - // Igualmente, puedes decidir el código de error a retornar. + return E_FAIL; } } // disconection sync root -HRESULT SyncRoot::DisconnectSyncRoot() +HRESULT SyncRoot::DisconnectSyncRoot(const wchar_t *syncRootPath) { - Logger::getInstance().log("Disconnecting sync root.", LogLevel::INFO); - try + auto it = connectionMap.find(syncRootPath); + if (it != connectionMap.end()) { - HRESULT hr = CfDisconnectSyncRoot(gloablConnectionKey); + HRESULT hr = CfDisconnectSyncRoot(it->second); + if (SUCCEEDED(hr)) + { + connectionMap.erase(it); + } return hr; } - catch (const std::exception &e) - { - Logger::getInstance().log("Exception caught: " + std::string(e.what()), LogLevel::ERROR); - } - catch (...) - { - Logger::getInstance().log("Unknown exception caught.", LogLevel::ERROR); - } + return E_FAIL; } // struct diff --git a/native-src/virtual_drive/Wrappers.cpp b/native-src/virtual_drive/Wrappers.cpp index a81e57e1..e8ba0e6e 100644 --- a/native-src/virtual_drive/Wrappers.cpp +++ b/native-src/virtual_drive/Wrappers.cpp @@ -112,19 +112,27 @@ napi_value UnregisterSyncRootWrapper(napi_env env, napi_callback_info args) if (argc < 1) { - napi_throw_error(env, nullptr, "The sync root path is required for UnregisterSyncRoot"); + napi_throw_error(env, nullptr, "The provider ID is required for UnregisterSyncRoot"); return nullptr; } - LPCWSTR syncRootPath; - size_t pathLength; - napi_get_value_string_utf16(env, argv[0], nullptr, 0, &pathLength); - syncRootPath = new WCHAR[pathLength + 1]; - napi_get_value_string_utf16(env, argv[0], reinterpret_cast(const_cast(syncRootPath)), pathLength + 1, nullptr); + GUID providerId; + LPCWSTR providerIdStr; + size_t providerIdStrLength; + napi_get_value_string_utf16(env, argv[0], nullptr, 0, &providerIdStrLength); + providerIdStr = new WCHAR[providerIdStrLength + 1]; + napi_get_value_string_utf16(env, argv[0], reinterpret_cast(const_cast(providerIdStr)), providerIdStrLength + 1, nullptr); - HRESULT result = SyncRoot::UnregisterSyncRoot(); + if (FAILED(CLSIDFromString(providerIdStr, &providerId))) + { + napi_throw_error(env, nullptr, "Invalid GUID format"); + delete[] providerIdStr; + return nullptr; + } - delete[] syncRootPath; + HRESULT result = SyncRoot::UnregisterSyncRoot(providerId); + + delete[] providerIdStr; napi_value napiResult; napi_create_int32(env, static_cast(result), &napiResult); @@ -429,8 +437,8 @@ napi_value DisconnectSyncRootWrapper(napi_env env, napi_callback_info args) syncRootPath = new WCHAR[pathLength + 1]; napi_get_value_string_utf16(env, argv[0], reinterpret_cast(const_cast(syncRootPath)), pathLength + 1, nullptr); - HRESULT result = SyncRoot::DisconnectSyncRoot(); - // wprintf(L"DisconnectSyncRootWrapper: %08x\n", static_cast(result)); + HRESULT result = SyncRoot::DisconnectSyncRoot(syncRootPath); + delete[] syncRootPath; napi_value napiResult; diff --git a/package.json b/package.json index dbc7e550..8e2ee658 100644 --- a/package.json +++ b/package.json @@ -1,52 +1,55 @@ { - "name": "virtual-drive", - "version": "1.0.1", - "description": "", - "main": "dist/index.ts", - "types": "dist/index.d.ts", - "scripts": { - "========== Testing ==========": "", - "test": "vitest", - "test:once": "yarn vitest --run", - "test:one": "yarn vitest related x", - "========== Build ==========": "", - "clean": "node-gyp clean", - "build:gyp": "node-gyp configure build", - "build:ts": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", - "config:gyp": "python gyp.config.py", - "build": "python gyp.config.py && node-gyp clean && node-gyp configure build && yarn build:ts", - "========== Examples ==========": "", - "prod:register": "node ./dist/examples/register.js", - "register": "nodemon", - "populate": "ts-node -r tsconfig-paths/register ./examples/populate.ts", - "get-state": "ts-node -r tsconfig-paths/register ./examples/get-state.ts", - "unregister": "ts-node -r tsconfig-paths/register ./examples/unregister.ts", - "disconnect": "ts-node -r tsconfig-paths/register ./examples/disconnect.ts" - }, - "author": "", - "license": "ISC", - "gypfile": true, - "devDependencies": { - "@trivago/prettier-plugin-sort-imports": "^5.2.1", - "@types/lodash.chunk": "^4.2.9", - "@types/node": "^20.5.0", - "@types/yargs": "^17.0.32", - "nodemon": "^3.1.9", - "prettier": "^3.4.2", - "ts-node": "^10.9.2", - "tsc-alias": "^1.8.10", - "typescript": "^5.1.6", - "vite-tsconfig-paths": "^5.1.4", - "vitest": "^3.0.3", - "vitest-mock-extended": "^2.0.2" - }, - "dependencies": { - "chokidar": "^3.6.0", - "lodash.chunk": "^4.2.0", - "tsconfig-paths": "^4.2.0", - "uuid": "^11.0.3", - "winston": "^3.17.0", - "yargs": "^17.7.2", - "zod": "^3.24.1" - } + "name": "virtual-drive", + "version": "1.0.1", + "description": "", + "main": "dist/index.ts", + "types": "dist/index.d.ts", + "scripts": { + "========== Testing ==========": "", + "test": "vitest", + "test:once": "yarn vitest --run", + "test:one": "yarn vitest related x", + "========== Build ==========": "", + "clean": "node-gyp clean", + "build:gyp": "node-gyp configure build", + "config:gyp": "python gyp.config.py", + "build:addon": "python gyp.config.py && node-gyp clean && node-gyp configure build", + "build:ts": "tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json", + "build": "npm run build:addon && npm run build:ts", + "========== Linter ==========": "", + "format": "prettier . --write", + "========== Examples ==========": "", + "prod:register": "node ./dist/examples/register.js", + "register": "nodemon", + "populate": "ts-node -r tsconfig-paths/register ./examples/populate.ts", + "get-state": "ts-node -r tsconfig-paths/register ./examples/get-state.ts", + "unregister": "ts-node -r tsconfig-paths/register ./examples/unregister.ts", + "disconnect": "ts-node -r tsconfig-paths/register ./examples/disconnect.ts" + }, + "author": "", + "license": "ISC", + "gypfile": true, + "devDependencies": { + "@trivago/prettier-plugin-sort-imports": "^5.2.1", + "@types/lodash.chunk": "^4.2.9", + "@types/node": "^20.5.0", + "@types/yargs": "^17.0.32", + "nodemon": "^3.1.9", + "prettier": "^3.4.2", + "ts-node": "^10.9.2", + "tsc-alias": "^1.8.10", + "typescript": "^5.1.6", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^3.0.3", + "vitest-mock-extended": "^2.0.2" + }, + "dependencies": { + "chokidar": "^3.6.0", + "lodash.chunk": "^4.2.0", + "tsconfig-paths": "^4.2.0", + "uuid": "^11.0.3", + "winston": "^3.17.0", + "yargs": "^17.7.2", + "zod": "^3.24.1" + } } diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..5add1165 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,2 @@ +sonar.projectKey=internxt_node-win +sonar.organization=internxt diff --git a/src/addon-wrapper.ts b/src/addon-wrapper.ts index 5e39f33a..14a2097b 100644 --- a/src/addon-wrapper.ts +++ b/src/addon-wrapper.ts @@ -32,13 +32,13 @@ export class Addon { return this.parseAddonZod("connectSyncRoot", result); } - unregisterSyncRoot({ syncRootPath }: { syncRootPath: string }) { - const result = addon.unregisterSyncRoot(syncRootPath); + unregisterSyncRoot({ providerId }: { providerId: string }) { + const result = addon.unregisterSyncRoot(providerId); return this.parseAddonZod("unregisterSyncRoot", result); } - disconnectSyncRoot() { - return addon.disconnectSyncRoot(this.syncRootPath); + disconnectSyncRoot({ syncRootPath }: { syncRootPath: string }) { + return addon.disconnectSyncRoot(syncRootPath); } addLogger({ logPath }: { logPath: string }) { diff --git a/src/queue/queueManager.ts b/src/queue/queueManager.ts index ada25d93..920db88a 100644 --- a/src/queue/queueManager.ts +++ b/src/queue/queueManager.ts @@ -18,9 +18,3 @@ export type HandleAction = (task: QueueItem) => Promise; export type HandleActions = { [key in typeQueue]: HandleAction; }; - -export interface IQueueManager { - actions: HandleActions; - - enqueue(task: QueueItem): void; -} diff --git a/src/types/callbacks.type.ts b/src/types/callbacks.type.ts index 96b09a5d..7e3be7d4 100644 --- a/src/types/callbacks.type.ts +++ b/src/types/callbacks.type.ts @@ -1,7 +1,11 @@ export type NapiCallbackFunction = (...args: any[]) => any; +export type FilePlaceholderIdPrefixType = "FILE:"; + +export type FilePlaceholderId = `${FilePlaceholderIdPrefixType}${string}`; + export type TFetchDataCallback = ( - id: string, + id: FilePlaceholderId, callback: (data: boolean, path: string, errorHandler?: () => void) => Promise<{ finished: boolean; progress: number }>, ) => void; diff --git a/src/virtual-drive.ts b/src/virtual-drive.ts index 3b881b24..3322b034 100644 --- a/src/virtual-drive.ts +++ b/src/virtual-drive.ts @@ -1,14 +1,12 @@ -import path, { join, win32 } from "path"; import fs from "fs"; -import { Watcher } from "./watcher/watcher"; -import { Callbacks } from "./types/callbacks.type"; -import { IQueueManager } from "./queue/queueManager"; - -import { createLogger } from "./logger"; -import { Addon } from "./addon-wrapper"; +import path, { join, win32 } from "path"; import winston from "winston"; -const addon = new Addon(); +import { Addon } from "./addon-wrapper"; +import { createLogger } from "./logger"; +import { QueueManager } from "./queue/queue-manager"; +import { Callbacks } from "./types/callbacks.type"; +import { Watcher } from "./watcher/watcher"; const PLACEHOLDER_ATTRIBUTES = { FILE_ATTRIBUTE_READONLY: 0x1, @@ -19,15 +17,20 @@ const PLACEHOLDER_ATTRIBUTES = { class VirtualDrive { syncRootPath: string; + providerId: string; callbacks?: Callbacks; watcher = new Watcher(); logger: winston.Logger; - constructor(syncRootPath: string, loggerPath: string) { + addon: Addon; + + constructor(syncRootPath: string, providerId: string, loggerPath: string) { + this.addon = new Addon(); this.syncRootPath = this.convertToWindowsPath(syncRootPath); loggerPath = this.convertToWindowsPath(loggerPath); + this.providerId = providerId; - addon.syncRootPath = this.syncRootPath; + this.addon.syncRootPath = this.syncRootPath; this.createSyncRootFolder(); this.addLoggerPath(loggerPath); @@ -52,15 +55,15 @@ class VirtualDrive { } addLoggerPath(logPath: string) { - addon.addLogger({ logPath }); + this.addon.addLogger({ logPath }); } getPlaceholderState(path: string) { - return addon.getPlaceholderState({ path: this.fixPath(path) }); + return this.addon.getPlaceholderState({ path: this.fixPath(path) }); } getPlaceholderWithStatePending() { - return addon.getPlaceholderWithStatePending(); + return this.addon.getPlaceholderWithStatePending(); } createSyncRootFolder() { @@ -70,11 +73,11 @@ class VirtualDrive { } getFileIdentity(relativePath: string) { - return addon.getFileIdentity({ path: this.fixPath(relativePath) }); + return this.addon.getFileIdentity({ path: this.fixPath(relativePath) }); } async deleteFileSyncRoot(relativePath: string) { - return addon.deleteFileSyncRoot({ path: this.fixPath(relativePath) }); + return this.addon.deleteFileSyncRoot({ path: this.fixPath(relativePath) }); } connectSyncRoot() { @@ -82,7 +85,14 @@ class VirtualDrive { throw new Error("Callbacks are not defined"); } - return addon.connectSyncRoot({ callbacks: this.callbacks }); + const connectionKey = this.addon.connectSyncRoot({ callbacks: this.callbacks }); + + this.logger.debug({ fn: "connectSyncRoot", connectionKey }); + return connectionKey; + } + + disconnectSyncRoot() { + this.addon.disconnectSyncRoot({ syncRootPath: this.syncRootPath }); } createPlaceholderFile( @@ -93,13 +103,13 @@ class VirtualDrive { creationTime: number, lastWriteTime: number, lastAccessTime: number, - basePath: string + basePath: string, ): any { const creationTimeStr = this.convertToWindowsTime(creationTime).toString(); const lastWriteTimeStr = this.convertToWindowsTime(lastWriteTime).toString(); const lastAccessTimeStr = this.convertToWindowsTime(lastAccessTime).toString(); - return addon.createPlaceholderFile({ + return this.addon.createPlaceholderFile({ fileName, fileId, fileSize, @@ -107,7 +117,7 @@ class VirtualDrive { creationTime: creationTimeStr, lastWriteTime: lastWriteTimeStr, lastAccessTime: lastAccessTimeStr, - basePath + basePath, }); } @@ -120,13 +130,13 @@ class VirtualDrive { creationTime: number, lastWriteTime: number, lastAccessTime: number, - path: string + path: string, ) { const creationTimeStr = this.convertToWindowsTime(creationTime).toString(); const lastWriteTimeStr = this.convertToWindowsTime(lastWriteTime).toString(); const lastAccessTimeStr = this.convertToWindowsTime(lastAccessTime).toString(); - - return addon.createPlaceholderDirectory({ + + return this.addon.createPlaceholderDirectory({ itemName, itemId, isDirectory, @@ -135,36 +145,27 @@ class VirtualDrive { creationTime: creationTimeStr, lastWriteTime: lastWriteTimeStr, lastAccessTime: lastAccessTimeStr, - path + path, }); } - async registerSyncRoot( - providerName: string, - providerVersion: string, - providerId: string, - callbacks: Callbacks, - logoPath: string - ): Promise { + async registerSyncRoot(providerName: string, providerVersion: string, callbacks: Callbacks, logoPath: string): Promise { this.callbacks = callbacks; - return addon.registerSyncRoot({ + console.log("Registering sync root: ", this.syncRootPath); + return this.addon.registerSyncRoot({ providerName, providerVersion, - providerId, - logoPath + providerId: this.providerId, + logoPath, }); } - static unregisterSyncRoot(syncRootPath: string) { - return addon.unregisterSyncRoot({ syncRootPath }); + unregisterSyncRoot() { + return this.addon.unregisterSyncRoot({ providerId: this.providerId }); } - watchAndWait( - path: string, - queueManager: IQueueManager, - loggerPath: string - ): void { - this.watcher.addon = addon; + watchAndWait(path: string, queueManager: QueueManager, loggerPath: string): void { + this.watcher.addon = this.addon; this.watcher.queueManager = queueManager; this.watcher.logger = this.logger; this.watcher.syncRootPath = this.syncRootPath; @@ -189,7 +190,7 @@ class VirtualDrive { itemId: string, size: number = 0, creationTime: number = Date.now(), - lastWriteTime: number = Date.now() + lastWriteTime: number = Date.now(), ) { const fullPath = path.join(this.syncRootPath, relativePath); const splitPath = relativePath.split("/").filter((p) => p); @@ -211,7 +212,7 @@ class VirtualDrive { creationTime, lastWriteTime, Date.now(), - currentPath + currentPath, ); } catch (error) { //@ts-ignore @@ -224,7 +225,7 @@ class VirtualDrive { itemId: string, size: number = 0, creationTime: number = Date.now(), - lastWriteTime: number = Date.now() + lastWriteTime: number = Date.now(), ) { const splitPath = relativePath.split("/").filter((p) => p); const directoryPath = path.resolve(this.syncRootPath); @@ -244,7 +245,7 @@ class VirtualDrive { creationTime, lastWriteTime, Date.now(), - currentPath + currentPath, ); } } @@ -252,31 +253,23 @@ class VirtualDrive { } } - disconnectSyncRoot() { - return addon.disconnectSyncRoot(); - } - - updateSyncStatus( - itemPath: string, - isDirectory: boolean, - sync: boolean = true - ) { - return addon.updateSyncStatus({ path: this.fixPath(itemPath), isDirectory, sync }); + updateSyncStatus(itemPath: string, isDirectory: boolean, sync: boolean = true) { + return this.addon.updateSyncStatus({ path: this.fixPath(itemPath), isDirectory, sync }); } convertToPlaceholder(itemPath: string, id: string) { - return addon.convertToPlaceholder({ path: this.fixPath(itemPath), id }); + return this.addon.convertToPlaceholder({ path: this.fixPath(itemPath), id }); } updateFileIdentity(itemPath: string, id: string, isDirectory: boolean) { - return addon.updateFileIdentity({ path: this.fixPath(itemPath), id, isDirectory }); + return this.addon.updateFileIdentity({ path: this.fixPath(itemPath), id, isDirectory }); } dehydrateFile(itemPath: string) { - return addon.dehydrateFile({ path: this.fixPath(itemPath) }); + return this.addon.dehydrateFile({ path: this.fixPath(itemPath) }); } hydrateFile(itemPath: string) { - return addon.hydrateFile({ path: this.fixPath(itemPath) }); + return this.addon.hydrateFile({ path: this.fixPath(itemPath) }); } } diff --git a/src/virtual-drive.unit.test.ts b/src/virtual-drive.unit.test.ts index 14c9c711..712d6366 100644 --- a/src/virtual-drive.unit.test.ts +++ b/src/virtual-drive.unit.test.ts @@ -29,8 +29,10 @@ describe("VirtualDrive", () => { }); describe("When convertToWindowsPath is called", () => { + const providerId = v4(); + // Arrange - const drive = new VirtualDrive(syncRootPath, logPath); + const drive = new VirtualDrive(syncRootPath, providerId, logPath); it("When unix path, then convert to windows path", () => { // Assert @@ -46,8 +48,10 @@ describe("VirtualDrive", () => { }); describe("When fixPath is called", () => { + const providerId = v4(); + // Arrange - const drive = new VirtualDrive(syncRootPath, logPath); + const drive = new VirtualDrive(syncRootPath, providerId, logPath); it("When absolute windows path, then do not modify it", () => { // Assert @@ -80,8 +84,10 @@ describe("VirtualDrive", () => { // Arrange mockExistsSync.mockReturnValue(false); + const providerId = v4(); + // Act - new VirtualDrive(syncRootPath, logPath); + new VirtualDrive(syncRootPath, providerId, logPath); // Assert expect(fs.mkdirSync).toHaveBeenCalledWith(syncRootPath, { @@ -93,8 +99,10 @@ describe("VirtualDrive", () => { // Arrange mockExistsSync.mockReturnValue(true); + const providerId = v4(); + // Act - new VirtualDrive(syncRootPath, logPath); + new VirtualDrive(syncRootPath, providerId, logPath); // Assert expect(fs.mkdirSync).not.toHaveBeenCalled(); @@ -102,7 +110,9 @@ describe("VirtualDrive", () => { it("Then it calls addon.addLoggerPath with logPath provided", () => { // Act - new VirtualDrive(syncRootPath, logPath); + const providerId = v4(); + + new VirtualDrive(syncRootPath, providerId, logPath); // Assert expect(addon.addLoggerPath).toHaveBeenCalledWith(logPath); @@ -113,7 +123,9 @@ describe("VirtualDrive", () => { it("Then it calls addon.createPlaceholderFile", () => { // Arrange mockExistsSync.mockReturnValue(true); - const drive = new VirtualDrive(syncRootPath, logPath); + const providerId = v4(); + + const drive = new VirtualDrive(syncRootPath, providerId, logPath); // Act drive.createFileByPath("folder/subfolder/file.txt", "file-id", 1234, 1660000000000, 1660000001000); @@ -135,16 +147,16 @@ describe("VirtualDrive", () => { describe("When call registerSyncRoot", () => { it("Then it assigns callbacks and calls addon.registerSyncRoot", async () => { // Arrange - const drive = new VirtualDrive(syncRootPath, logPath); + const providerId = v4(); + const drive = new VirtualDrive(syncRootPath, providerId, logPath); const providerName = "MyProvider"; const providerVersion = "1.0.0"; - const providerId = v4(); const logoPath = "C:\\iconPath"; const callbacks = mockDeep(); // Act expect(drive.callbacks).toBe(undefined); - await drive.registerSyncRoot(providerName, providerVersion, providerId, callbacks, logoPath); + await drive.registerSyncRoot(providerName, providerVersion, callbacks, logoPath); // Assert expect(drive.callbacks).not.toBe(undefined); diff --git a/src/watcher/watcher.ts b/src/watcher/watcher.ts index 2695b968..c185dc8a 100644 --- a/src/watcher/watcher.ts +++ b/src/watcher/watcher.ts @@ -4,21 +4,16 @@ import { Logger } from "winston"; import { Addon } from "@/addon-wrapper"; import { QueueManager } from "@/queue/queue-manager"; -import { IQueueManager } from "@/queue/queueManager"; import { OnAddDirService } from "./events/on-add-dir.service"; import { OnAddService } from "./events/on-add.service"; import { OnRawService } from "./events/on-raw.service"; -export namespace Watcher { - export type TOptions = WatchOptions; -} - export class Watcher { syncRootPath!: string; - options!: Watcher.TOptions; + options!: WatchOptions; addon!: Addon; - queueManager!: IQueueManager; + queueManager!: QueueManager; logger!: Logger; fileInDevice = new Set(); chokidar?: FSWatcher;