Refactor WebDAV path construction and fix note move operations
- Replace Node.js fetch with Electron net.request for better session handling - Extract WebDAV path building into reusable private methods with proper URL encoding - Add helper methods for category path encoding and attachment path construction - Fix note move operations to use remote category/filename from saved snapshots - Add ensureCategoryDirectoryExists to handle nested category creation - Only move/rename attachment folders when note has any
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
const fs = require('node:fs/promises');
|
const fs = require('node:fs/promises');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
|
const { app, BrowserWindow, dialog, ipcMain, net } = require('electron');
|
||||||
|
|
||||||
const rendererUrl = process.env.ELECTRON_RENDERER_URL;
|
const rendererUrl = process.env.ELECTRON_RENDERER_URL;
|
||||||
const isDev = Boolean(rendererUrl);
|
const isDev = Boolean(rendererUrl);
|
||||||
@@ -90,23 +90,55 @@ ipcMain.handle('desktop:http-request', async (_event, payload) => {
|
|||||||
const body =
|
const body =
|
||||||
payload.bodyBase64 != null
|
payload.bodyBase64 != null
|
||||||
? Buffer.from(payload.bodyBase64, 'base64')
|
? Buffer.from(payload.bodyBase64, 'base64')
|
||||||
: payload.bodyText;
|
: payload.bodyText != null
|
||||||
|
? Buffer.from(payload.bodyText, 'utf8')
|
||||||
|
: null;
|
||||||
|
|
||||||
const response = await fetch(payload.url, {
|
return await new Promise((resolve, reject) => {
|
||||||
method: payload.method || 'GET',
|
const request = net.request({
|
||||||
headers: payload.headers,
|
url: payload.url,
|
||||||
body,
|
method: payload.method || 'GET',
|
||||||
|
session: BrowserWindow.getAllWindows()[0]?.webContents.session,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [name, value] of Object.entries(payload.headers || {})) {
|
||||||
|
request.setHeader(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.on('response', (response) => {
|
||||||
|
const chunks = [];
|
||||||
|
|
||||||
|
response.on('data', (chunk) => {
|
||||||
|
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
||||||
|
});
|
||||||
|
|
||||||
|
response.on('end', () => {
|
||||||
|
const headers = {};
|
||||||
|
for (const [name, value] of Object.entries(response.headers)) {
|
||||||
|
headers[name] = Array.isArray(value) ? value.join(', ') : String(value ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
resolve({
|
||||||
|
ok: response.statusCode >= 200 && response.statusCode < 300,
|
||||||
|
status: response.statusCode,
|
||||||
|
statusText: response.statusMessage || '',
|
||||||
|
headers,
|
||||||
|
bodyBase64: buffer.toString('base64'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
response.on('error', reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on('error', reject);
|
||||||
|
|
||||||
|
if (body && body.length > 0) {
|
||||||
|
request.write(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
request.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
const buffer = Buffer.from(await response.arrayBuffer());
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: response.ok,
|
|
||||||
status: response.status,
|
|
||||||
statusText: response.statusText,
|
|
||||||
headers: Object.fromEntries(response.headers.entries()),
|
|
||||||
bodyBase64: buffer.toString('base64'),
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('desktop:export-pdf', async (event, payload) => {
|
ipcMain.handle('desktop:export-pdf', async (event, payload) => {
|
||||||
|
|||||||
15
src/App.tsx
15
src/App.tsx
@@ -510,10 +510,21 @@ function MainApp() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const remoteCategory = getRemoteCategory(note);
|
const savedSnapshot = note.draftId ? savedSnapshotsRef.current.get(note.draftId) : null;
|
||||||
|
const remoteReference = savedSnapshot ?? note;
|
||||||
|
const remoteCategory = getRemoteCategory(remoteReference);
|
||||||
|
|
||||||
if (remoteCategory !== note.category) {
|
if (remoteCategory !== note.category) {
|
||||||
const movedNote = await syncManager.moveNote(note, note.category);
|
const movedNote = await syncManager.moveNote(
|
||||||
|
{
|
||||||
|
...note,
|
||||||
|
id: remoteReference.id,
|
||||||
|
path: remoteReference.path,
|
||||||
|
filename: remoteReference.filename,
|
||||||
|
category: remoteCategory,
|
||||||
|
},
|
||||||
|
note.category,
|
||||||
|
);
|
||||||
return syncManager.updateNote({
|
return syncManager.updateNote({
|
||||||
...movedNote,
|
...movedNote,
|
||||||
draftId: note.draftId,
|
draftId: note.draftId,
|
||||||
|
|||||||
@@ -127,17 +127,8 @@ export class NextcloudAPI {
|
|||||||
// Build WebDAV path: /remote.php/dav/files/{username}/Notes/{category}/.attachments.{noteId}/{filename}
|
// Build WebDAV path: /remote.php/dav/files/{username}/Notes/{category}/.attachments.{noteId}/{filename}
|
||||||
// The path from markdown is like: .attachments.38479/Screenshot.png
|
// The path from markdown is like: .attachments.38479/Screenshot.png
|
||||||
// We need to construct the full WebDAV URL
|
// We need to construct the full WebDAV URL
|
||||||
|
|
||||||
let webdavPath = `/remote.php/dav/files/${this.username}/Notes`;
|
const webdavPath = this.buildAttachmentWebDAVPath(noteCategory, path);
|
||||||
|
|
||||||
// Add category subfolder if present
|
|
||||||
if (noteCategory) {
|
|
||||||
webdavPath += `/${noteCategory}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the attachment path (already includes .attachments.{id}/filename)
|
|
||||||
webdavPath += `/${path}`;
|
|
||||||
|
|
||||||
const url = `${this.serverURL}${webdavPath}`;
|
const url = `${this.serverURL}${webdavPath}`;
|
||||||
console.log(`[Note ${_noteId}] Fetching attachment via WebDAV:`, url);
|
console.log(`[Note ${_noteId}] Fetching attachment via WebDAV:`, url);
|
||||||
|
|
||||||
@@ -167,13 +158,7 @@ export class NextcloudAPI {
|
|||||||
async uploadAttachment(noteId: number | string, file: File, noteCategory?: string): Promise<string> {
|
async uploadAttachment(noteId: number | string, file: File, noteCategory?: string): Promise<string> {
|
||||||
// Create .attachments.{noteId} directory path and upload file via WebDAV PUT
|
// Create .attachments.{noteId} directory path and upload file via WebDAV PUT
|
||||||
// Returns the relative path to insert into markdown
|
// Returns the relative path to insert into markdown
|
||||||
|
|
||||||
let webdavPath = `/remote.php/dav/files/${this.username}/Notes`;
|
|
||||||
|
|
||||||
if (noteCategory) {
|
|
||||||
webdavPath += `/${noteCategory}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize note ID: extract just the filename without extension and remove invalid chars
|
// Sanitize note ID: extract just the filename without extension and remove invalid chars
|
||||||
// noteId might be "category/filename.md" or just "filename.md"
|
// noteId might be "category/filename.md" or just "filename.md"
|
||||||
const noteIdStr = String(noteId);
|
const noteIdStr = String(noteId);
|
||||||
@@ -183,7 +168,7 @@ export class NextcloudAPI {
|
|||||||
|
|
||||||
const attachmentDir = `.attachments.${sanitizedNoteId}`;
|
const attachmentDir = `.attachments.${sanitizedNoteId}`;
|
||||||
const fileName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_'); // Sanitize filename
|
const fileName = file.name.replace(/[^a-zA-Z0-9._-]/g, '_'); // Sanitize filename
|
||||||
const fullPath = `${webdavPath}/${attachmentDir}/${fileName}`;
|
const fullPath = this.buildAttachmentWebDAVPath(noteCategory, attachmentDir, fileName);
|
||||||
|
|
||||||
const url = `${this.serverURL}${fullPath}`;
|
const url = `${this.serverURL}${fullPath}`;
|
||||||
console.log('Uploading attachment via WebDAV:', url);
|
console.log('Uploading attachment via WebDAV:', url);
|
||||||
@@ -191,7 +176,7 @@ export class NextcloudAPI {
|
|||||||
// First, try to create the attachments directory (MKCOL)
|
// First, try to create the attachments directory (MKCOL)
|
||||||
// This may fail if it already exists, which is fine
|
// This may fail if it already exists, which is fine
|
||||||
try {
|
try {
|
||||||
await runtimeFetch(`${this.serverURL}${webdavPath}/${attachmentDir}`, {
|
await runtimeFetch(`${this.serverURL}${this.buildAttachmentWebDAVPath(noteCategory, attachmentDir)}`, {
|
||||||
method: 'MKCOL',
|
method: 'MKCOL',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': this.authHeader,
|
'Authorization': this.authHeader,
|
||||||
@@ -223,7 +208,7 @@ export class NextcloudAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchCategoryColors(): Promise<Record<string, number>> {
|
async fetchCategoryColors(): Promise<Record<string, number>> {
|
||||||
const webdavPath = `/remote.php/dav/files/${this.username}/Notes/.category-colors.json`;
|
const webdavPath = `${this.buildNotesRootWebDAVPath()}/.category-colors.json`;
|
||||||
const url = `${this.serverURL}${webdavPath}`;
|
const url = `${this.serverURL}${webdavPath}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -250,7 +235,7 @@ export class NextcloudAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveCategoryColors(colors: Record<string, number>): Promise<void> {
|
async saveCategoryColors(colors: Record<string, number>): Promise<void> {
|
||||||
const webdavPath = `/remote.php/dav/files/${this.username}/Notes/.category-colors.json`;
|
const webdavPath = `${this.buildNotesRootWebDAVPath()}/.category-colors.json`;
|
||||||
const url = `${this.serverURL}${webdavPath}`;
|
const url = `${this.serverURL}${webdavPath}`;
|
||||||
|
|
||||||
const content = JSON.stringify(colors, null, 2);
|
const content = JSON.stringify(colors, null, 2);
|
||||||
@@ -294,9 +279,99 @@ export class NextcloudAPI {
|
|||||||
return note.content;
|
return note.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildNotesRootWebDAVPath(): string {
|
||||||
|
return `/remote.php/dav/files/${this.username}/Notes`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildEncodedCategoryPath(category: string): string {
|
||||||
|
if (!category) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const encodedSegments = category
|
||||||
|
.split('/')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((segment) => encodeURIComponent(segment));
|
||||||
|
|
||||||
|
return encodedSegments.length ? `/${encodedSegments.join('/')}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildCategoryWebDAVPath(category: string): string {
|
||||||
|
return `${this.buildNotesRootWebDAVPath()}${this.buildEncodedCategoryPath(category)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRemoteCategoryForNote(note: Note): string {
|
||||||
|
if (note.path) {
|
||||||
|
const pathParts = note.path.split('/');
|
||||||
|
return pathParts.length > 1 ? pathParts.slice(0, -1).join('/') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof note.id === 'string') {
|
||||||
|
const idParts = note.id.split('/');
|
||||||
|
return idParts.length > 1 ? idParts.slice(0, -1).join('/') : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return note.category;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getRemoteFilenameForNote(note: Note): string {
|
||||||
|
if (note.filename) {
|
||||||
|
return note.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.path) {
|
||||||
|
return note.path.split('/').pop() || 'note.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof note.id === 'string') {
|
||||||
|
return note.id.split('/').pop() || 'note.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'note.md';
|
||||||
|
}
|
||||||
|
|
||||||
private buildNoteWebDAVPath(category: string, filename: string): string {
|
private buildNoteWebDAVPath(category: string, filename: string): string {
|
||||||
const categoryPath = category ? `/${category}` : '';
|
return `${this.buildCategoryWebDAVPath(category)}/${encodeURIComponent(filename)}`;
|
||||||
return `/remote.php/dav/files/${this.username}/Notes${categoryPath}/${encodeURIComponent(filename)}`;
|
}
|
||||||
|
|
||||||
|
private buildRelativeWebDAVPath(...segments: string[]): string {
|
||||||
|
return segments
|
||||||
|
.flatMap((segment) => segment.split('/'))
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((segment) => encodeURIComponent(segment))
|
||||||
|
.join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildAttachmentWebDAVPath(noteCategory: string | undefined, ...relativeSegments: string[]): string {
|
||||||
|
const relativePath = this.buildRelativeWebDAVPath(...relativeSegments);
|
||||||
|
return relativePath
|
||||||
|
? `${this.buildCategoryWebDAVPath(noteCategory || '')}/${relativePath}`
|
||||||
|
: this.buildCategoryWebDAVPath(noteCategory || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private noteHasLocalAttachments(note: Note): boolean {
|
||||||
|
return note.content.includes('.attachments.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureCategoryDirectoryExists(category: string): Promise<void> {
|
||||||
|
if (!category) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = category.split('/').filter(Boolean);
|
||||||
|
|
||||||
|
for (let index = 0; index < parts.length; index += 1) {
|
||||||
|
const currentCategory = parts.slice(0, index + 1).join('/');
|
||||||
|
const categoryUrl = `${this.serverURL}${this.buildCategoryWebDAVPath(currentCategory)}`;
|
||||||
|
const response = await runtimeFetch(categoryUrl, {
|
||||||
|
method: 'MKCOL',
|
||||||
|
headers: { 'Authorization': this.authHeader },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok && response.status !== 405) {
|
||||||
|
throw createHttpStatusError(`Failed to create category folder: ${response.status}`, response.status);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async delay(ms: number): Promise<void> {
|
private async delay(ms: number): Promise<void> {
|
||||||
@@ -361,7 +436,7 @@ export class NextcloudAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchNotesWebDAV(): Promise<Note[]> {
|
async fetchNotesWebDAV(): Promise<Note[]> {
|
||||||
const webdavPath = `/remote.php/dav/files/${this.username}/Notes`;
|
const webdavPath = this.buildNotesRootWebDAVPath();
|
||||||
const url = `${this.serverURL}${webdavPath}`;
|
const url = `${this.serverURL}${webdavPath}`;
|
||||||
|
|
||||||
const response = await runtimeFetch(url, {
|
const response = await runtimeFetch(url, {
|
||||||
@@ -439,9 +514,8 @@ export class NextcloudAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchNoteContentWebDAV(note: Note): Promise<Note> {
|
async fetchNoteContentWebDAV(note: Note): Promise<Note> {
|
||||||
const categoryPath = note.category ? `/${note.category}` : '';
|
|
||||||
const filename = note.filename || String(note.id).split('/').pop() || 'note.md';
|
const filename = note.filename || String(note.id).split('/').pop() || 'note.md';
|
||||||
const webdavPath = `/remote.php/dav/files/${this.username}/Notes${categoryPath}/${encodeURIComponent(filename)}`;
|
const webdavPath = this.buildNoteWebDAVPath(note.category, filename);
|
||||||
const url = `${this.serverURL}${webdavPath}`;
|
const url = `${this.serverURL}${webdavPath}`;
|
||||||
|
|
||||||
const response = await runtimeFetch(url, {
|
const response = await runtimeFetch(url, {
|
||||||
@@ -458,21 +532,12 @@ export class NextcloudAPI {
|
|||||||
|
|
||||||
async createNoteWebDAV(title: string, content: string, category: string): Promise<Note> {
|
async createNoteWebDAV(title: string, content: string, category: string): Promise<Note> {
|
||||||
const filename = `${title.replace(/[\/:\*?"<>|]/g, '').replace(/\s+/g, ' ').trim()}.md`;
|
const filename = `${title.replace(/[\/:\*?"<>|]/g, '').replace(/\s+/g, ' ').trim()}.md`;
|
||||||
const categoryPath = category ? `/${category}` : '';
|
const webdavPath = this.buildNoteWebDAVPath(category, filename);
|
||||||
const webdavPath = `/remote.php/dav/files/${this.username}/Notes${categoryPath}/${encodeURIComponent(filename)}`;
|
|
||||||
const url = `${this.serverURL}${webdavPath}`;
|
const url = `${this.serverURL}${webdavPath}`;
|
||||||
|
|
||||||
// Ensure category directory exists
|
// Ensure category directory exists
|
||||||
if (category) {
|
if (category) {
|
||||||
try {
|
await this.ensureCategoryDirectoryExists(category);
|
||||||
const categoryUrl = `${this.serverURL}/remote.php/dav/files/${this.username}/Notes/${category}`;
|
|
||||||
await runtimeFetch(categoryUrl, {
|
|
||||||
method: 'MKCOL',
|
|
||||||
headers: { 'Authorization': this.authHeader },
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// Directory might already exist
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const noteContent = content ? `${title}\n${content}` : title;
|
const noteContent = content ? `${title}\n${content}` : title;
|
||||||
@@ -616,31 +681,32 @@ export class NextcloudAPI {
|
|||||||
throw createHttpStatusError(`Failed to rename note: ${response.status}`, response.status);
|
throw createHttpStatusError(`Failed to rename note: ${response.status}`, response.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also rename attachment folder if it exists
|
if (this.noteHasLocalAttachments(note)) {
|
||||||
const categoryPath = note.category ? `/${note.category}` : '';
|
// Also rename attachment folder if the note references local attachments
|
||||||
const oldNoteIdStr = String(note.id);
|
const oldNoteIdStr = String(note.id);
|
||||||
const oldJustFilename = oldNoteIdStr.split('/').pop() || oldNoteIdStr;
|
const oldJustFilename = oldNoteIdStr.split('/').pop() || oldNoteIdStr;
|
||||||
const oldFilenameWithoutExt = oldJustFilename.replace(/\.(md|txt)$/, '');
|
const oldFilenameWithoutExt = oldJustFilename.replace(/\.(md|txt)$/, '');
|
||||||
const oldSanitizedNoteId = oldFilenameWithoutExt.replace(/[^a-zA-Z0-9_-]/g, '_');
|
const oldSanitizedNoteId = oldFilenameWithoutExt.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
const oldAttachmentFolder = `.attachments.${oldSanitizedNoteId}`;
|
const oldAttachmentFolder = `.attachments.${oldSanitizedNoteId}`;
|
||||||
|
|
||||||
const newFilenameWithoutExt = newFilename.replace(/\.(md|txt)$/, '');
|
const newFilenameWithoutExt = newFilename.replace(/\.(md|txt)$/, '');
|
||||||
const newSanitizedNoteId = newFilenameWithoutExt.replace(/[^a-zA-Z0-9_-]/g, '_');
|
const newSanitizedNoteId = newFilenameWithoutExt.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
const newAttachmentFolder = `.attachments.${newSanitizedNoteId}`;
|
const newAttachmentFolder = `.attachments.${newSanitizedNoteId}`;
|
||||||
|
|
||||||
const oldAttachmentPath = `/remote.php/dav/files/${this.username}/Notes${categoryPath}/${oldAttachmentFolder}`;
|
const oldAttachmentPath = this.buildAttachmentWebDAVPath(note.category, oldAttachmentFolder);
|
||||||
const newAttachmentPath = `/remote.php/dav/files/${this.username}/Notes${categoryPath}/${newAttachmentFolder}`;
|
const newAttachmentPath = this.buildAttachmentWebDAVPath(note.category, newAttachmentFolder);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runtimeFetch(`${this.serverURL}${oldAttachmentPath}`, {
|
await runtimeFetch(`${this.serverURL}${oldAttachmentPath}`, {
|
||||||
method: 'MOVE',
|
method: 'MOVE',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': this.authHeader,
|
'Authorization': this.authHeader,
|
||||||
'Destination': `${this.serverURL}${newAttachmentPath}`,
|
'Destination': `${this.serverURL}${newAttachmentPath}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Attachment folder might not exist, that's ok
|
// Attachment folder might not exist, that's ok
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshedMetadata = await this.tryFetchNoteMetadataWebDAV(note.category, newFilename);
|
const refreshedMetadata = await this.tryFetchNoteMetadataWebDAV(note.category, newFilename);
|
||||||
@@ -657,8 +723,7 @@ export class NextcloudAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteNoteWebDAV(note: Note): Promise<void> {
|
async deleteNoteWebDAV(note: Note): Promise<void> {
|
||||||
const categoryPath = note.category ? `/${note.category}` : '';
|
const webdavPath = this.buildNoteWebDAVPath(note.category, note.filename!);
|
||||||
const webdavPath = `/remote.php/dav/files/${this.username}/Notes${categoryPath}/${encodeURIComponent(note.filename!)}`;
|
|
||||||
const url = `${this.serverURL}${webdavPath}`;
|
const url = `${this.serverURL}${webdavPath}`;
|
||||||
|
|
||||||
const response = await runtimeFetch(url, {
|
const response = await runtimeFetch(url, {
|
||||||
@@ -672,28 +737,14 @@ export class NextcloudAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async moveNoteWebDAV(note: Note, newCategory: string): Promise<Note> {
|
async moveNoteWebDAV(note: Note, newCategory: string): Promise<Note> {
|
||||||
const oldCategoryPath = note.category ? `/${note.category}` : '';
|
const remoteCategory = this.getRemoteCategoryForNote(note);
|
||||||
const newCategoryPath = newCategory ? `/${newCategory}` : '';
|
const remoteFilename = this.getRemoteFilenameForNote(note);
|
||||||
const oldPath = `/remote.php/dav/files/${this.username}/Notes${oldCategoryPath}/${encodeURIComponent(note.filename!)}`;
|
const oldPath = this.buildNoteWebDAVPath(remoteCategory, remoteFilename);
|
||||||
const newPath = `/remote.php/dav/files/${this.username}/Notes${newCategoryPath}/${encodeURIComponent(note.filename!)}`;
|
const newPath = this.buildNoteWebDAVPath(newCategory, remoteFilename);
|
||||||
|
|
||||||
// Ensure new category directory exists (including nested subdirectories)
|
// Ensure new category directory exists (including nested subdirectories)
|
||||||
if (newCategory) {
|
if (newCategory) {
|
||||||
const parts = newCategory.split('/');
|
await this.ensureCategoryDirectoryExists(newCategory);
|
||||||
let currentPath = '';
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
currentPath += (currentPath ? '/' : '') + part;
|
|
||||||
try {
|
|
||||||
const categoryUrl = `${this.serverURL}/remote.php/dav/files/${this.username}/Notes/${currentPath}`;
|
|
||||||
await runtimeFetch(categoryUrl, {
|
|
||||||
method: 'MKCOL',
|
|
||||||
headers: { 'Authorization': this.authHeader },
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
// Directory might already exist, continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await runtimeFetch(`${this.serverURL}${oldPath}`, {
|
const response = await runtimeFetch(`${this.serverURL}${oldPath}`, {
|
||||||
@@ -705,48 +756,56 @@ export class NextcloudAPI {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok && response.status !== 201 && response.status !== 204) {
|
if (!response.ok && response.status !== 201 && response.status !== 204) {
|
||||||
throw new Error(`Failed to move note: ${response.status}`);
|
const details = await response.text().catch(() => '');
|
||||||
|
const detailSuffix = details ? ` - ${details.slice(0, 300)}` : '';
|
||||||
|
throw createHttpStatusError(
|
||||||
|
`Failed to move note: ${response.status}${detailSuffix}. Source: ${oldPath}. Destination: ${newPath}`,
|
||||||
|
response.status,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move attachment folder if it exists
|
if (this.noteHasLocalAttachments(note)) {
|
||||||
const noteIdStr = String(note.id);
|
// Move attachment folder only when the note references local attachments
|
||||||
const justFilename = noteIdStr.split('/').pop() || noteIdStr;
|
const noteIdStr = String(note.id);
|
||||||
const filenameWithoutExt = justFilename.replace(/\.(md|txt)$/, '');
|
const justFilename = noteIdStr.split('/').pop() || noteIdStr;
|
||||||
const sanitizedNoteId = filenameWithoutExt.replace(/[^a-zA-Z0-9_-]/g, '_');
|
const filenameWithoutExt = justFilename.replace(/\.(md|txt)$/, '');
|
||||||
const attachmentFolder = `.attachments.${sanitizedNoteId}`;
|
const sanitizedNoteId = filenameWithoutExt.replace(/[^a-zA-Z0-9_-]/g, '_');
|
||||||
|
const attachmentFolder = `.attachments.${sanitizedNoteId}`;
|
||||||
const oldAttachmentPath = `/remote.php/dav/files/${this.username}/Notes${oldCategoryPath}/${attachmentFolder}`;
|
|
||||||
const newAttachmentPath = `/remote.php/dav/files/${this.username}/Notes${newCategoryPath}/${attachmentFolder}`;
|
const oldAttachmentPath = this.buildAttachmentWebDAVPath(remoteCategory, attachmentFolder);
|
||||||
|
const newAttachmentPath = this.buildAttachmentWebDAVPath(newCategory, attachmentFolder);
|
||||||
console.log(`Attempting to move attachment folder:`);
|
|
||||||
console.log(` From: ${oldAttachmentPath}`);
|
|
||||||
console.log(` To: ${newAttachmentPath}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const attachmentResponse = await runtimeFetch(`${this.serverURL}${oldAttachmentPath}`, {
|
|
||||||
method: 'MOVE',
|
|
||||||
headers: {
|
|
||||||
'Authorization': this.authHeader,
|
|
||||||
'Destination': `${this.serverURL}${newAttachmentPath}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`Attachment folder MOVE response status: ${attachmentResponse.status}`);
|
console.log(`Attempting to move attachment folder:`);
|
||||||
|
console.log(` From: ${oldAttachmentPath}`);
|
||||||
|
console.log(` To: ${newAttachmentPath}`);
|
||||||
|
|
||||||
if (attachmentResponse.ok || attachmentResponse.status === 201 || attachmentResponse.status === 204) {
|
try {
|
||||||
console.log(`✓ Successfully moved attachment folder: ${attachmentFolder}`);
|
const attachmentResponse = await runtimeFetch(`${this.serverURL}${oldAttachmentPath}`, {
|
||||||
} else {
|
method: 'MOVE',
|
||||||
console.log(`✗ Failed to move attachment folder (status ${attachmentResponse.status})`);
|
headers: {
|
||||||
|
'Authorization': this.authHeader,
|
||||||
|
'Destination': `${this.serverURL}${newAttachmentPath}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Attachment folder MOVE response status: ${attachmentResponse.status}`);
|
||||||
|
|
||||||
|
if (attachmentResponse.ok || attachmentResponse.status === 201 || attachmentResponse.status === 204) {
|
||||||
|
console.log(`✓ Successfully moved attachment folder: ${attachmentFolder}`);
|
||||||
|
} else {
|
||||||
|
console.log(`✗ Failed to move attachment folder (status ${attachmentResponse.status})`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`✗ Error moving attachment folder:`, e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.log(`✗ Error moving attachment folder:`, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...note,
|
...note,
|
||||||
category: newCategory,
|
category: newCategory,
|
||||||
path: newCategory ? `${newCategory}/${note.filename}` : note.filename || '',
|
filename: remoteFilename,
|
||||||
id: newCategory ? `${newCategory}/${note.filename}` : (note.filename || ''),
|
path: newCategory ? `${newCategory}/${remoteFilename}` : remoteFilename,
|
||||||
|
id: newCategory ? `${newCategory}/${remoteFilename}` : remoteFilename,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user