feat: allow projects to have external data path for posts and media
This commit is contained in:
@@ -49,20 +49,28 @@ export interface MediaMetadata {
|
||||
|
||||
export class MediaEngine extends EventEmitter {
|
||||
private currentProjectId: string = 'default';
|
||||
private projectBaseDir: string | null = null;
|
||||
private dataDir: string | null = null; // For media files (may be external)
|
||||
private internalDir: string | null = null; // For thumbnails (always local)
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private getProjectBaseDir(): string {
|
||||
if (this.projectBaseDir) return this.projectBaseDir;
|
||||
private getDefaultBaseDir(): string {
|
||||
const userDataPath = app.getPath('userData');
|
||||
return path.join(userDataPath, 'projects', this.currentProjectId);
|
||||
}
|
||||
|
||||
private getDataDir(): string {
|
||||
return this.dataDir || this.getDefaultBaseDir();
|
||||
}
|
||||
|
||||
private getInternalDir(): string {
|
||||
return this.internalDir || this.getDefaultBaseDir();
|
||||
}
|
||||
|
||||
private getMediaBaseDir(): string {
|
||||
return path.join(this.getProjectBaseDir(), 'media');
|
||||
return path.join(this.getDataDir(), 'media');
|
||||
}
|
||||
|
||||
private getMediaDir(): string {
|
||||
@@ -91,9 +99,11 @@ export class MediaEngine extends EventEmitter {
|
||||
return path.join(dir, `${id}${extension}`);
|
||||
}
|
||||
|
||||
setProjectContext(projectId: string, baseDir?: string): void {
|
||||
setProjectContext(projectId: string, dataDir?: string, internalDir?: string): void {
|
||||
this.currentProjectId = projectId;
|
||||
this.projectBaseDir = baseDir || null;
|
||||
this.dataDir = dataDir || null;
|
||||
this.internalDir = internalDir || null;
|
||||
console.log(`[MediaEngine] setProjectContext: projectId=${projectId}, dataDir=${this.dataDir}, internalDir=${this.internalDir}`);
|
||||
}
|
||||
|
||||
getProjectContext(): string {
|
||||
@@ -108,7 +118,7 @@ export class MediaEngine extends EventEmitter {
|
||||
* Get the thumbnails directory for the current project
|
||||
*/
|
||||
private getThumbnailsDir(): string {
|
||||
return path.join(this.getProjectBaseDir(), 'thumbnails');
|
||||
return path.join(this.getInternalDir(), 'thumbnails');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -543,6 +553,7 @@ export class MediaEngine extends EventEmitter {
|
||||
|
||||
async rebuildDatabaseFromFiles(): Promise<void> {
|
||||
const mediaBaseDir = this.getMediaBaseDir();
|
||||
console.log(`[MediaEngine] rebuildDatabaseFromFiles: scanning mediaBaseDir=${mediaBaseDir}`);
|
||||
const task: Task<void> = {
|
||||
id: uuidv4(),
|
||||
name: 'Rebuild database from media files',
|
||||
|
||||
@@ -32,7 +32,6 @@ export const DEFAULT_CATEGORIES = ['article', 'picture', 'aside', 'page'];
|
||||
*/
|
||||
export class MetaEngine extends EventEmitter {
|
||||
private currentProjectId: string = 'default';
|
||||
private projectBaseDir: string | null = null;
|
||||
private tags: Set<string> = new Set();
|
||||
private categories: Set<string> = new Set();
|
||||
private projectMetadata: ProjectMetadata | null = null;
|
||||
@@ -42,18 +41,21 @@ export class MetaEngine extends EventEmitter {
|
||||
super();
|
||||
}
|
||||
|
||||
private getProjectBaseDir(): string {
|
||||
if (this.projectBaseDir) return this.projectBaseDir;
|
||||
/**
|
||||
* Always returns the internal project directory (in userData).
|
||||
* Meta files never live in an external dataPath.
|
||||
*/
|
||||
private getInternalBaseDir(): string {
|
||||
const userDataPath = app.getPath('userData');
|
||||
return path.join(userDataPath, 'projects', this.currentProjectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the meta directory path for the current project.
|
||||
* Format: {baseDir}/meta/
|
||||
* Always in the internal directory (userData), never external.
|
||||
*/
|
||||
getMetaDir(): string {
|
||||
return path.join(this.getProjectBaseDir(), 'meta');
|
||||
return path.join(this.getInternalBaseDir(), 'meta');
|
||||
}
|
||||
|
||||
private getTagsFilePath(): string {
|
||||
@@ -68,9 +70,8 @@ export class MetaEngine extends EventEmitter {
|
||||
return path.join(this.getMetaDir(), 'project.json');
|
||||
}
|
||||
|
||||
setProjectContext(projectId: string, baseDir?: string): void {
|
||||
setProjectContext(projectId: string): void {
|
||||
this.currentProjectId = projectId;
|
||||
this.projectBaseDir = baseDir || null;
|
||||
// Reset in-memory cache when project changes
|
||||
this.tags.clear();
|
||||
this.categories.clear();
|
||||
|
||||
@@ -142,16 +142,16 @@ export class PostEngine extends EventEmitter {
|
||||
await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [id] });
|
||||
}
|
||||
|
||||
private projectBaseDir: string | null = null;
|
||||
private dataDir: string | null = null;
|
||||
|
||||
private getProjectBaseDir(): string {
|
||||
if (this.projectBaseDir) return this.projectBaseDir;
|
||||
private getDataDir(): string {
|
||||
if (this.dataDir) return this.dataDir;
|
||||
const userDataPath = app.getPath('userData');
|
||||
return path.join(userDataPath, 'projects', this.currentProjectId);
|
||||
}
|
||||
|
||||
private getPostsBaseDir(): string {
|
||||
return path.join(this.getProjectBaseDir(), 'posts');
|
||||
return path.join(this.getDataDir(), 'posts');
|
||||
}
|
||||
|
||||
private getPostsDir(): string {
|
||||
@@ -179,9 +179,9 @@ export class PostEngine extends EventEmitter {
|
||||
return path.join(dir, `${slug}.md`);
|
||||
}
|
||||
|
||||
setProjectContext(projectId: string, baseDir?: string): void {
|
||||
setProjectContext(projectId: string, dataDir?: string): void {
|
||||
this.currentProjectId = projectId;
|
||||
this.projectBaseDir = baseDir || null;
|
||||
this.dataDir = dataDir || null;
|
||||
}
|
||||
|
||||
getProjectContext(): string {
|
||||
|
||||
@@ -31,37 +31,42 @@ export class ProjectEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base directory for a project's data.
|
||||
* If the project has a custom dataPath, use that; otherwise use the default.
|
||||
* Get the internal base directory for a project (always in userData).
|
||||
* This is where meta, thumbnails, tags, and project.json live.
|
||||
*/
|
||||
getProjectBaseDir(projectId: string, dataPath?: string | null): string {
|
||||
if (dataPath) {
|
||||
return dataPath;
|
||||
}
|
||||
getInternalBaseDir(projectId: string): string {
|
||||
const userDataPath = app.getPath('userData');
|
||||
return path.join(userDataPath, 'projects', projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default base directory (in userData) for a project.
|
||||
* Get the data directory for posts and media.
|
||||
* If a custom dataPath is set, posts/media live there; otherwise in the internal dir.
|
||||
*/
|
||||
getDataDir(projectId: string, dataPath?: string | null): string {
|
||||
if (dataPath) {
|
||||
return dataPath;
|
||||
}
|
||||
return this.getInternalBaseDir(projectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias kept for backward compatibility — returns the internal base dir.
|
||||
*/
|
||||
getDefaultProjectBaseDir(projectId: string): string {
|
||||
const userDataPath = app.getPath('userData');
|
||||
return path.join(userDataPath, 'projects', projectId);
|
||||
return this.getInternalBaseDir(projectId);
|
||||
}
|
||||
|
||||
private async ensureProjectDirectories(projectId: string, dataPath?: string | null): Promise<void> {
|
||||
const projectDir = this.getProjectBaseDir(projectId, dataPath);
|
||||
const postsDir = path.join(projectDir, 'posts');
|
||||
const mediaDir = path.join(projectDir, 'media');
|
||||
const thumbnailsDir = path.join(projectDir, 'thumbnails');
|
||||
const metaDir = path.join(projectDir, 'meta');
|
||||
// Internal directories (always in userData)
|
||||
const internalDir = this.getInternalBaseDir(projectId);
|
||||
await fs.mkdir(path.join(internalDir, 'thumbnails'), { recursive: true });
|
||||
await fs.mkdir(path.join(internalDir, 'meta'), { recursive: true });
|
||||
|
||||
await fs.mkdir(projectDir, { recursive: true });
|
||||
await fs.mkdir(postsDir, { recursive: true });
|
||||
await fs.mkdir(mediaDir, { recursive: true });
|
||||
await fs.mkdir(thumbnailsDir, { recursive: true });
|
||||
await fs.mkdir(metaDir, { recursive: true });
|
||||
// Data directories (may be external)
|
||||
const dataDir = this.getDataDir(projectId, dataPath);
|
||||
await fs.mkdir(path.join(dataDir, 'posts'), { recursive: true });
|
||||
await fs.mkdir(path.join(dataDir, 'media'), { recursive: true });
|
||||
}
|
||||
|
||||
async createProject(data: { name: string; description?: string; slug?: string; dataPath?: string }): Promise<ProjectData> {
|
||||
@@ -194,14 +199,13 @@ export class ProjectEngine extends EventEmitter {
|
||||
// Delete associated media from database
|
||||
await db.delete(media).where(eq(media.projectId, id));
|
||||
|
||||
// Delete project files and directories
|
||||
const paths = this.getProjectPaths(id);
|
||||
// Delete the internal project directory (meta, thumbnails, and posts/media if stored internally).
|
||||
// If a custom dataPath is set, external posts/media are NOT deleted — the user manages that storage.
|
||||
const internalDir = this.getInternalBaseDir(id);
|
||||
try {
|
||||
// Delete posts directory
|
||||
await fs.rm(path.dirname(paths.posts), { recursive: true, force: true });
|
||||
await fs.rm(internalDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
// Directory may not exist, that's okay
|
||||
console.warn(`Could not delete project directory for ${id}:`, error);
|
||||
console.warn(`Could not delete internal project directory for ${id}:`, error);
|
||||
}
|
||||
|
||||
// Delete project from database
|
||||
@@ -287,10 +291,10 @@ export class ProjectEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
getProjectPaths(projectId: string, dataPath?: string | null): { posts: string; media: string } {
|
||||
const baseDir = this.getProjectBaseDir(projectId, dataPath);
|
||||
const dataDir = this.getDataDir(projectId, dataPath);
|
||||
return {
|
||||
posts: path.join(baseDir, 'posts'),
|
||||
media: path.join(baseDir, 'media'),
|
||||
posts: path.join(dataDir, 'posts'),
|
||||
media: path.join(dataDir, 'media'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -110,14 +110,16 @@ function isValidHexColor(color: string): boolean {
|
||||
*/
|
||||
export class TagEngine extends EventEmitter {
|
||||
private currentProjectId: string = 'default';
|
||||
private projectBaseDir: string | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
private getProjectBaseDir(): string {
|
||||
if (this.projectBaseDir) return this.projectBaseDir;
|
||||
/**
|
||||
* Always returns the internal project directory (in userData).
|
||||
* Tag metadata never lives in an external dataPath.
|
||||
*/
|
||||
private getInternalBaseDir(): string {
|
||||
const userDataPath = app.getPath('userData');
|
||||
return path.join(userDataPath, 'projects', this.currentProjectId);
|
||||
}
|
||||
@@ -125,9 +127,8 @@ export class TagEngine extends EventEmitter {
|
||||
/**
|
||||
* Set the current project context
|
||||
*/
|
||||
setProjectContext(projectId: string, baseDir?: string): void {
|
||||
setProjectContext(projectId: string): void {
|
||||
this.currentProjectId = projectId;
|
||||
this.projectBaseDir = baseDir || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -141,7 +142,7 @@ export class TagEngine extends EventEmitter {
|
||||
* Get the tags file path for filesystem persistence
|
||||
*/
|
||||
private getTagsFilePath(): string {
|
||||
return path.join(this.getProjectBaseDir(), 'meta', 'tags-metadata.json');
|
||||
return path.join(this.getInternalBaseDir(), 'meta', 'tags-metadata.json');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user