feat: Add function to check if file exists before upload
This commit is contained in:
@@ -7,20 +7,76 @@ import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { FileError, NetworkError } from "../../errors/DownloadErrors";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { CloudDriveConfig } from "./types";
|
||||
import { getFileList } from "./fileLister";
|
||||
import { normalizeUploadPath } from "./pathUtils";
|
||||
import { CloudDriveConfig } from "./types";
|
||||
|
||||
/**
|
||||
* Upload result indicating whether file was actually uploaded or skipped
|
||||
*/
|
||||
export interface UploadResult {
|
||||
uploaded: boolean;
|
||||
skipped: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file already exists in cloud storage
|
||||
* @param fileName - Name of the file to check
|
||||
* @param fileSize - Size of the file in bytes
|
||||
* @param destinationPath - Full destination path in cloud storage
|
||||
* @param config - Cloud drive configuration
|
||||
* @returns true if file exists with same name and size
|
||||
*/
|
||||
async function fileExistsInCloud(
|
||||
fileName: string,
|
||||
fileSize: number,
|
||||
destinationPath: string,
|
||||
config: CloudDriveConfig
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
// Get the directory path from destination path
|
||||
const dirPath = path.dirname(destinationPath);
|
||||
const normalizedDirPath = normalizeUploadPath(dirPath);
|
||||
|
||||
// Get file list from OpenList
|
||||
const files = await getFileList(config, normalizedDirPath);
|
||||
|
||||
// Check if file with same name and size exists
|
||||
const existingFile = files.find(
|
||||
(file) => file.name === fileName && file.size === fileSize && !file.is_dir
|
||||
);
|
||||
|
||||
if (existingFile) {
|
||||
logger.info(
|
||||
`[CloudStorage] File ${fileName} already exists in cloud storage with same size (${fileSize} bytes), skipping upload`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`[CloudStorage] Failed to check if file exists in cloud storage:`,
|
||||
error instanceof Error ? error : new Error(String(error))
|
||||
);
|
||||
// If check fails, proceed with upload to be safe
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to cloud storage
|
||||
* @param filePath - Local file path to upload
|
||||
* @param config - Cloud drive configuration
|
||||
* @param remotePath - Optional remote path (relative to uploadPath)
|
||||
* @returns UploadResult indicating whether file was uploaded or skipped
|
||||
*/
|
||||
export async function uploadFile(
|
||||
filePath: string,
|
||||
config: CloudDriveConfig,
|
||||
remotePath?: string
|
||||
): Promise<void> {
|
||||
): Promise<UploadResult> {
|
||||
// 1. Get basic file information
|
||||
const fileStat = fs.statSync(filePath);
|
||||
const fileSize = fileStat.size;
|
||||
@@ -55,13 +111,27 @@ export async function uploadFile(
|
||||
? destinationPath
|
||||
: `/${destinationPath}`;
|
||||
|
||||
// Check if file already exists in cloud storage before uploading
|
||||
const exists = await fileExistsInCloud(
|
||||
fileName,
|
||||
fileSize,
|
||||
destinationPath,
|
||||
config
|
||||
);
|
||||
|
||||
if (exists) {
|
||||
return {
|
||||
uploaded: false,
|
||||
skipped: true,
|
||||
reason: "File already exists in cloud storage with same size",
|
||||
};
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[CloudStorage] Uploading ${fileName} to ${destinationPath} (${fileSize} bytes)...`
|
||||
);
|
||||
|
||||
logger.debug(
|
||||
`[CloudStorage] Destination path in header: ${destinationPath}`
|
||||
);
|
||||
logger.debug(`[CloudStorage] Destination path in header: ${destinationPath}`);
|
||||
|
||||
// 3. Prepare Headers
|
||||
const headers = {
|
||||
@@ -92,6 +162,10 @@ export async function uploadFile(
|
||||
logger.info(
|
||||
`[CloudStorage] Successfully uploaded ${fileName}. Server message: ${response.data.message}`
|
||||
);
|
||||
return {
|
||||
uploaded: true,
|
||||
skipped: false,
|
||||
};
|
||||
} else {
|
||||
// Even if HTTP status code is 200, server may return business errors
|
||||
const errorMessage = response.data
|
||||
@@ -132,4 +206,3 @@ export async function uploadFile(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,15 @@ import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import { logger } from "../../utils/logger";
|
||||
import { updateVideo } from "../storageService";
|
||||
import { CloudDriveConfig, FileType } from "./types";
|
||||
import { resolveAbsolutePath, sanitizeFilename } from "./pathUtils";
|
||||
import { uploadFile } from "./fileUploader";
|
||||
import { clearSignedUrlCache } from "./urlSigner";
|
||||
import { clearFileListCache } from "./fileLister";
|
||||
import { normalizeUploadPath } from "./pathUtils";
|
||||
import { uploadFile, UploadResult } from "./fileUploader";
|
||||
import {
|
||||
normalizeUploadPath,
|
||||
resolveAbsolutePath,
|
||||
sanitizeFilename,
|
||||
} from "./pathUtils";
|
||||
import { CloudDriveConfig } from "./types";
|
||||
import { clearSignedUrlCache } from "./urlSigner";
|
||||
|
||||
/**
|
||||
* Upload video, thumbnail, and metadata to cloud storage
|
||||
@@ -25,14 +28,24 @@ export async function uploadVideo(
|
||||
logger.info(`[CloudStorage] Starting upload for video: ${videoData.title}`);
|
||||
|
||||
const uploadedFiles: string[] = []; // Track successfully uploaded files for deletion
|
||||
const filesToUpdate: string[] = []; // Track files that should update database (uploaded or skipped)
|
||||
|
||||
try {
|
||||
// Upload Video File
|
||||
if (videoData.videoPath) {
|
||||
const absoluteVideoPath = resolveAbsolutePath(videoData.videoPath);
|
||||
if (absoluteVideoPath && fs.existsSync(absoluteVideoPath)) {
|
||||
await uploadFile(absoluteVideoPath, config);
|
||||
uploadedFiles.push(absoluteVideoPath);
|
||||
const uploadResult: UploadResult = await uploadFile(
|
||||
absoluteVideoPath,
|
||||
config
|
||||
);
|
||||
if (uploadResult.uploaded) {
|
||||
uploadedFiles.push(absoluteVideoPath);
|
||||
}
|
||||
// Track file for database update whether uploaded or skipped
|
||||
if (uploadResult.uploaded || uploadResult.skipped) {
|
||||
filesToUpdate.push(absoluteVideoPath);
|
||||
}
|
||||
} else {
|
||||
logger.error(
|
||||
`[CloudStorage] Video file not found: ${videoData.videoPath}`
|
||||
@@ -45,8 +58,17 @@ export async function uploadVideo(
|
||||
if (videoData.thumbnailPath) {
|
||||
const absoluteThumbPath = resolveAbsolutePath(videoData.thumbnailPath);
|
||||
if (absoluteThumbPath && fs.existsSync(absoluteThumbPath)) {
|
||||
await uploadFile(absoluteThumbPath, config);
|
||||
uploadedFiles.push(absoluteThumbPath);
|
||||
const uploadResult: UploadResult = await uploadFile(
|
||||
absoluteThumbPath,
|
||||
config
|
||||
);
|
||||
if (uploadResult.uploaded) {
|
||||
uploadedFiles.push(absoluteThumbPath);
|
||||
}
|
||||
// Track file for database update whether uploaded or skipped
|
||||
if (uploadResult.uploaded || uploadResult.skipped) {
|
||||
filesToUpdate.push(absoluteThumbPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,14 +97,17 @@ export async function uploadVideo(
|
||||
fs.ensureDirSync(path.dirname(metadataPath));
|
||||
fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
||||
|
||||
await uploadFile(metadataPath, config);
|
||||
const metadataUploadResult: UploadResult = await uploadFile(
|
||||
metadataPath,
|
||||
config
|
||||
);
|
||||
|
||||
// Cleanup temp metadata (always delete temp file)
|
||||
fs.unlinkSync(metadataPath);
|
||||
|
||||
logger.info(`[CloudStorage] Upload completed for: ${videoData.title}`);
|
||||
|
||||
// Delete local files after successful upload and update video record to point to cloud storage
|
||||
// Delete local files after successful upload (only for files that were actually uploaded)
|
||||
if (uploadedFiles.length > 0) {
|
||||
logger.info(
|
||||
`[CloudStorage] Deleting ${uploadedFiles.length} local file(s) after successful upload...`
|
||||
@@ -108,59 +133,60 @@ export async function uploadVideo(
|
||||
}
|
||||
}
|
||||
logger.info(`[CloudStorage] Local file cleanup completed`);
|
||||
}
|
||||
|
||||
// Update video record to point to cloud storage (store only filename, not full URL with sign)
|
||||
// Sign will be retrieved dynamically when needed
|
||||
if (videoData.id && deletedFiles.length > 0) {
|
||||
try {
|
||||
const updates: any = {};
|
||||
// Update video record to point to cloud storage (store only filename, not full URL with sign)
|
||||
// Sign will be retrieved dynamically when needed
|
||||
// Update database if files were uploaded OR skipped (already in cloud)
|
||||
if (videoData.id && filesToUpdate.length > 0) {
|
||||
try {
|
||||
const updates: any = {};
|
||||
|
||||
// Store cloud storage indicator in path format: "cloud:filename"
|
||||
// This allows us to identify cloud storage files and retrieve sign dynamically
|
||||
const videoFilename =
|
||||
videoData.videoFilename ||
|
||||
(videoData.videoPath ? path.basename(videoData.videoPath) : null);
|
||||
// Store cloud storage indicator in path format: "cloud:filename"
|
||||
// This allows us to identify cloud storage files and retrieve sign dynamically
|
||||
const videoFilename =
|
||||
videoData.videoFilename ||
|
||||
(videoData.videoPath ? path.basename(videoData.videoPath) : null);
|
||||
|
||||
if (videoFilename) {
|
||||
updates.videoPath = `cloud:${videoFilename}`;
|
||||
}
|
||||
|
||||
const thumbnailFilename =
|
||||
videoData.thumbnailFilename ||
|
||||
(videoData.thumbnailPath
|
||||
? path.basename(videoData.thumbnailPath)
|
||||
: null);
|
||||
|
||||
if (thumbnailFilename) {
|
||||
updates.thumbnailPath = `cloud:${thumbnailFilename}`;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
updateVideo(videoData.id, updates);
|
||||
logger.info(
|
||||
`[CloudStorage] Updated video record ${videoData.id} with cloud storage indicators`
|
||||
);
|
||||
|
||||
// Clear cache for uploaded files to ensure fresh URLs
|
||||
if (videoFilename) {
|
||||
clearSignedUrlCache(videoFilename, "video");
|
||||
}
|
||||
if (thumbnailFilename) {
|
||||
clearSignedUrlCache(thumbnailFilename, "thumbnail");
|
||||
}
|
||||
// Also clear file list cache since new files were added
|
||||
const uploadPath = normalizeUploadPath(config.uploadPath);
|
||||
clearFileListCache(uploadPath);
|
||||
}
|
||||
} catch (updateError: any) {
|
||||
logger.error(
|
||||
`[CloudStorage] Failed to update video record with cloud paths:`,
|
||||
updateError instanceof Error
|
||||
? updateError
|
||||
: new Error(updateError.message)
|
||||
);
|
||||
// Don't throw - file deletion was successful
|
||||
if (videoFilename) {
|
||||
updates.videoPath = `cloud:${videoFilename}`;
|
||||
}
|
||||
|
||||
const thumbnailFilename =
|
||||
videoData.thumbnailFilename ||
|
||||
(videoData.thumbnailPath
|
||||
? path.basename(videoData.thumbnailPath)
|
||||
: null);
|
||||
|
||||
if (thumbnailFilename) {
|
||||
updates.thumbnailPath = `cloud:${thumbnailFilename}`;
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
updateVideo(videoData.id, updates);
|
||||
logger.info(
|
||||
`[CloudStorage] Updated video record ${videoData.id} with cloud storage indicators`
|
||||
);
|
||||
|
||||
// Clear cache for uploaded files to ensure fresh URLs
|
||||
if (videoFilename) {
|
||||
clearSignedUrlCache(videoFilename, "video");
|
||||
}
|
||||
if (thumbnailFilename) {
|
||||
clearSignedUrlCache(thumbnailFilename, "thumbnail");
|
||||
}
|
||||
// Also clear file list cache since new files were added
|
||||
const uploadPath = normalizeUploadPath(config.uploadPath);
|
||||
clearFileListCache(uploadPath);
|
||||
}
|
||||
} catch (updateError: any) {
|
||||
logger.error(
|
||||
`[CloudStorage] Failed to update video record with cloud paths:`,
|
||||
updateError instanceof Error
|
||||
? updateError
|
||||
: new Error(updateError.message)
|
||||
);
|
||||
// Don't throw - file deletion was successful
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -171,4 +197,3 @@ export async function uploadVideo(
|
||||
// If upload failed, don't delete local files
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user