Files
Myxeliums_Battlemap_Importe…/scripts/lib/media-storage-service.js
2026-01-08 04:35:14 +01:00

173 lines
6.1 KiB
JavaScript

/**
* Media Storage Service
*
* Handles uploading background media files (images and videos) to Foundry VTT's
* file storage system. Manages directory creation and file organization.
*
* @module MediaStorageService
*/
/** Module identifier for console logging */
const MODULE_LOG_PREFIX = 'Quick Battlemap Importer';
/** Storage source for file operations (Foundry's data directory) */
const STORAGE_SOURCE = 'data';
/**
* @typedef {Object} BackgroundMediaData
* @property {string} data - Base64 data URL or blob URL for the media
* @property {string} filename - Original filename of the media
* @property {File} [file] - The original File object (if available)
* @property {boolean} isVideo - Whether the media is a video file
*/
/**
* @typedef {Object} UploadResult
* @property {string} path - The path to the uploaded file in Foundry's storage
*/
/**
* Service class responsible for uploading media files to Foundry's storage.
* Handles directory creation and file upload with error handling.
*/
export class MediaStorageService {
/**
* Upload a background media file (image or video) to Foundry's storage.
* Creates the target directory if it doesn't exist.
*
* @param {BackgroundMediaData} mediaData - The media data to upload
* @param {string} worldId - The current world's identifier for directory naming
* @returns {Promise<UploadResult|null>} Upload result with file path, or null on failure
*
* @example
* const storage = new MediaStorageService();
* const result = await storage.uploadBackgroundMedia(mediaData, game.world.id);
* if (result?.path) {
* // Use result.path as the scene background
* }
*/
async uploadBackgroundMedia(mediaData, worldId) {
try {
// Get or create a File object from the media data
const fileToUpload = await this.prepareFileForUpload(mediaData);
// Build the target directory path
const targetDirectory = this.buildTargetDirectory(worldId);
// Ensure the directory exists
await this.ensureDirectoryExists(STORAGE_SOURCE, targetDirectory);
// Upload the file
const uploadResult = await FilePicker.upload(
STORAGE_SOURCE,
targetDirectory,
fileToUpload,
{ overwrite: true }
);
return { path: uploadResult?.path };
} catch (uploadError) {
this.handleUploadError(uploadError);
return null;
}
}
/**
* Prepare a File object for upload from media data.
* If a File object already exists, uses it directly.
* Otherwise, fetches the data URL and converts to a File.
*
* @param {BackgroundMediaData} mediaData - The media data
* @returns {Promise<File>} A File object ready for upload
*/
async prepareFileForUpload(mediaData) {
// If we already have a File object, use it directly
if (mediaData.file) {
return mediaData.file;
}
// Otherwise, fetch the data URL and create a File from it
const response = await fetch(mediaData.data);
const blobData = await response.blob();
// Determine MIME type from blob or based on media type
const mimeType = blobData.type || this.getDefaultMimeType(mediaData.isVideo);
return new File([blobData], mediaData.filename, { type: mimeType });
}
/**
* Get the default MIME type based on whether the media is video or image.
*
* @param {boolean} isVideo - Whether the media is a video
* @returns {string} The default MIME type
*/
getDefaultMimeType(isVideo) {
return isVideo ? 'video/webm' : 'image/png';
}
/**
* Build the target directory path for storing battlemap media.
* Uses the world ID to organize files by world.
*
* @param {string} worldId - The world identifier
* @returns {string} The target directory path
*/
buildTargetDirectory(worldId) {
return `worlds/${worldId}/quick-battlemap`;
}
/**
* Ensure a directory exists in Foundry's file storage.
* Creates the directory if it doesn't exist, handling race conditions.
*
* @param {string} storageSource - The storage source (typically 'data')
* @param {string} directoryPath - The path to the directory
* @throws {Error} If directory creation fails for reasons other than already existing
*/
async ensureDirectoryExists(storageSource, directoryPath) {
try {
// Try to browse the directory to see if it exists
await FilePicker.browse(storageSource, directoryPath);
} catch (_browseError) {
// Directory doesn't exist, try to create it
await this.createDirectorySafely(storageSource, directoryPath);
}
}
/**
* Safely create a directory, handling the case where it already exists.
* Multiple simultaneous requests might try to create the same directory.
*
* @param {string} storageSource - The storage source
* @param {string} directoryPath - The path to create
*/
async createDirectorySafely(storageSource, directoryPath) {
try {
await FilePicker.createDirectory(storageSource, directoryPath, {});
} catch (createError) {
// EEXIST means directory was created by another request - that's fine
const errorMessage = String(createError || '');
if (!errorMessage.includes('EEXIST')) {
throw createError;
}
}
}
/**
* Handle upload errors by logging and notifying the user.
*
* @param {Error} uploadError - The error that occurred
*/
handleUploadError(uploadError) {
console.error(`${MODULE_LOG_PREFIX} | Upload failed:`, uploadError);
const localizedMessage = game.i18n.localize('QUICKBATTLEMAP.UploadFailed');
const errorDetail = uploadError?.message ?? String(uploadError);
ui.notifications.error(`${localizedMessage}: ${errorDetail}`);
}
}