|
@@ -2,6 +2,7 @@ import { MD5 } from '@pura/harmony-utils';
|
|
|
import { WebdavManager } from '../util/WebdavManager';
|
|
import { WebdavManager } from '../util/WebdavManager';
|
|
|
import FileManager, { merge2paths } from '../util/FileManager';
|
|
import FileManager, { merge2paths } from '../util/FileManager';
|
|
|
import { WebDavAccount } from '../../viewmodel/WebDavAccount';
|
|
import { WebDavAccount } from '../../viewmodel/WebDavAccount';
|
|
|
|
|
+import Logger from '../util/Logger';
|
|
|
import nativeBridge from 'libentry.so';
|
|
import nativeBridge from 'libentry.so';
|
|
|
|
|
|
|
|
interface SmbDownloadBinding {
|
|
interface SmbDownloadBinding {
|
|
@@ -10,6 +11,88 @@ interface SmbDownloadBinding {
|
|
|
|
|
|
|
|
const smbBinding: SmbDownloadBinding = nativeBridge as SmbDownloadBinding;
|
|
const smbBinding: SmbDownloadBinding = nativeBridge as SmbDownloadBinding;
|
|
|
|
|
|
|
|
|
|
+function normalizeRelativePath(relativePath: string): string {
|
|
|
|
|
+ if (!relativePath || relativePath.length === 0) {
|
|
|
|
|
+ return '/';
|
|
|
|
|
+ }
|
|
|
|
|
+ let normalized = relativePath.replace(/\\/g, '/').replace(/\/+/g, '/');
|
|
|
|
|
+ if (!normalized.startsWith('/')) {
|
|
|
|
|
+ normalized = `/${normalized}`;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (normalized.length > 1 && normalized.endsWith('/')) {
|
|
|
|
|
+ normalized = normalized.slice(0, -1);
|
|
|
|
|
+ }
|
|
|
|
|
+ return normalized || '/';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function cleanShareName(name?: string): string {
|
|
|
|
|
+ return name ? name.replace(/^\/+|\/+$/g, '') : '';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function buildRemotePathCandidates(relativePath: string, account: WebDavAccount): string[] {
|
|
|
|
|
+ const ordered: string[] = [];
|
|
|
|
|
+ const seen = new Set<string>();
|
|
|
|
|
+ const pushCandidate = (value: string) => {
|
|
|
|
|
+ if (!value || value.length === 0) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ const normalized = normalizeRelativePath(value);
|
|
|
|
|
+ if (!seen.has(normalized)) {
|
|
|
|
|
+ seen.add(normalized);
|
|
|
|
|
+ ordered.push(normalized);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const base = normalizeRelativePath(relativePath);
|
|
|
|
|
+ pushCandidate(base);
|
|
|
|
|
+
|
|
|
|
|
+ const trimmed = base.replace(/^\/+/, '');
|
|
|
|
|
+ if (trimmed.length === 0) {
|
|
|
|
|
+ return ordered;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const shareNames: string[] = [];
|
|
|
|
|
+ const accountShare = cleanShareName(account.smbShare);
|
|
|
|
|
+ if (accountShare.length > 0) {
|
|
|
|
|
+ shareNames.push(accountShare);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < shareNames.length; i++) {
|
|
|
|
|
+ const share = shareNames[i];
|
|
|
|
|
+ const lowerShare = share.toLowerCase();
|
|
|
|
|
+ if (trimmed.toLowerCase().startsWith(`${lowerShare}/`)) {
|
|
|
|
|
+ const remainder = trimmed.substring(share.length + 1);
|
|
|
|
|
+ if (remainder.length > 0) {
|
|
|
|
|
+ pushCandidate(remainder);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const slashIndex = trimmed.indexOf('/');
|
|
|
|
|
+ if (slashIndex > 0) {
|
|
|
|
|
+ const firstSegment = trimmed.substring(0, slashIndex);
|
|
|
|
|
+ const remainder = trimmed.substring(slashIndex + 1);
|
|
|
|
|
+ if (remainder.length > 0) {
|
|
|
|
|
+ const matchesShare = shareNames.some(name => name.toLowerCase() === firstSegment.toLowerCase());
|
|
|
|
|
+ if (!matchesShare) {
|
|
|
|
|
+ pushCandidate(remainder);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return ordered;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function shouldRetryRemoteDownload(error: Error): boolean {
|
|
|
|
|
+ if (!error || !error.message) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ const message = error.message.toLowerCase();
|
|
|
|
|
+ return message.indexOf('failed to open remote file') >= 0 ||
|
|
|
|
|
+ message.indexOf('object_name_not_found') >= 0 ||
|
|
|
|
|
+ message.indexOf('no such file') >= 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function buildCacheFileName(relativePath: string): Promise<string> {
|
|
async function buildCacheFileName(relativePath: string): Promise<string> {
|
|
|
const hash = await MD5.digestSync(relativePath ?? '');
|
|
const hash = await MD5.digestSync(relativePath ?? '');
|
|
|
const lastSlash = relativePath.lastIndexOf('/');
|
|
const lastSlash = relativePath.lastIndexOf('/');
|
|
@@ -34,7 +117,7 @@ export async function ensureSmbFileCached(account: WebDavAccount, relativePath:
|
|
|
await FileManager.createDir(cacheRoot);
|
|
await FileManager.createDir(cacheRoot);
|
|
|
const accountDir = merge2paths(cacheRoot, account.id?.toString() ?? 'default');
|
|
const accountDir = merge2paths(cacheRoot, account.id?.toString() ?? 'default');
|
|
|
await FileManager.createDir(accountDir);
|
|
await FileManager.createDir(accountDir);
|
|
|
- const normalizedRelative = relativePath?.startsWith('/') ? relativePath : `/${relativePath}`;
|
|
|
|
|
|
|
+ const normalizedRelative = normalizeRelativePath(relativePath);
|
|
|
const cacheFileName = await buildCacheFileName(normalizedRelative);
|
|
const cacheFileName = await buildCacheFileName(normalizedRelative);
|
|
|
const localPath = merge2paths(accountDir, cacheFileName);
|
|
const localPath = merge2paths(accountDir, cacheFileName);
|
|
|
let exists = await FileManager.isExist(localPath);
|
|
let exists = await FileManager.isExist(localPath);
|
|
@@ -47,20 +130,38 @@ export async function ensureSmbFileCached(account: WebDavAccount, relativePath:
|
|
|
}
|
|
}
|
|
|
if (!exists) {
|
|
if (!exists) {
|
|
|
const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
|
|
const host = account.isUseLocalHost && account.localHost ? account.localHost : account.host;
|
|
|
- try {
|
|
|
|
|
- smbBinding.downloadSmbFile(
|
|
|
|
|
- host,
|
|
|
|
|
- account.smbShare,
|
|
|
|
|
- account.account,
|
|
|
|
|
- account.password,
|
|
|
|
|
- account.smbDomain,
|
|
|
|
|
- normalizedRelative,
|
|
|
|
|
- localPath
|
|
|
|
|
- );
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- await FileManager.deleteFile(localPath);
|
|
|
|
|
- const err = error as Error;
|
|
|
|
|
- throw err;
|
|
|
|
|
|
|
+ const candidates = buildRemotePathCandidates(normalizedRelative, account);
|
|
|
|
|
+ let lastError: Error | undefined;
|
|
|
|
|
+ for (let i = 0; i < candidates.length; i++) {
|
|
|
|
|
+ const candidate = candidates[i];
|
|
|
|
|
+ try {
|
|
|
|
|
+ smbBinding.downloadSmbFile(
|
|
|
|
|
+ host,
|
|
|
|
|
+ account.smbShare,
|
|
|
|
|
+ account.account,
|
|
|
|
|
+ account.password,
|
|
|
|
|
+ account.smbDomain,
|
|
|
|
|
+ candidate,
|
|
|
|
|
+ localPath
|
|
|
|
|
+ );
|
|
|
|
|
+ if (i > 0) {
|
|
|
|
|
+ Logger.warn('SMB download fallback succeeded with path:', candidate);
|
|
|
|
|
+ }
|
|
|
|
|
+ lastError = undefined;
|
|
|
|
|
+ break;
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ await FileManager.deleteFile(localPath);
|
|
|
|
|
+ const err = error as Error;
|
|
|
|
|
+ lastError = err;
|
|
|
|
|
+ const shouldRetry = i < candidates.length - 1 && shouldRetryRemoteDownload(err);
|
|
|
|
|
+ if (!shouldRetry) {
|
|
|
|
|
+ throw err;
|
|
|
|
|
+ }
|
|
|
|
|
+ Logger.warn('SMB download failed for path, trying fallback:', candidate);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (lastError) {
|
|
|
|
|
+ throw lastError;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
return localPath;
|
|
return localPath;
|