update nextcloud for file support
This commit is contained in:
@@ -24,11 +24,13 @@
|
||||
"jose": "^6.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"pg": "^8.18.0",
|
||||
"multer": "^1.4.5-lts.2",
|
||||
"winston": "^3.19.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/multer": "^1.4.12",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^25.3.0",
|
||||
|
||||
@@ -167,6 +167,117 @@ class NextcloudController {
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFile(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const credentials = await userService.getNextcloudCredentials(req.user!.id);
|
||||
if (!credentials) {
|
||||
res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' });
|
||||
return;
|
||||
}
|
||||
const token = req.params.token as string;
|
||||
if (!token) {
|
||||
res.status(400).json({ success: false, message: 'Room token fehlt' });
|
||||
return;
|
||||
}
|
||||
if (!req.file) {
|
||||
res.status(400).json({ success: false, message: 'Keine Datei übermittelt' });
|
||||
return;
|
||||
}
|
||||
await nextcloudService.uploadFileToTalk(
|
||||
token,
|
||||
req.file.buffer,
|
||||
req.file.originalname,
|
||||
req.file.mimetype,
|
||||
credentials.loginName,
|
||||
credentials.appPassword,
|
||||
);
|
||||
res.status(200).json({ success: true, data: null });
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'NEXTCLOUD_AUTH_INVALID') {
|
||||
await userService.clearNextcloudCredentials(req.user!.id);
|
||||
res.status(200).json({ success: true, data: { connected: false } });
|
||||
return;
|
||||
}
|
||||
logger.error('uploadFile error', { error });
|
||||
res.status(500).json({ success: false, message: 'Datei konnte nicht hochgeladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async downloadFile(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const credentials = await userService.getNextcloudCredentials(req.user!.id);
|
||||
if (!credentials) {
|
||||
res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' });
|
||||
return;
|
||||
}
|
||||
const filePath = req.query.path as string;
|
||||
if (!filePath) {
|
||||
res.status(400).json({ success: false, message: 'Dateipfad fehlt' });
|
||||
return;
|
||||
}
|
||||
const response = await nextcloudService.downloadFile(
|
||||
filePath,
|
||||
credentials.loginName,
|
||||
credentials.appPassword,
|
||||
);
|
||||
const contentType = response.headers['content-type'] ?? 'application/octet-stream';
|
||||
const contentDisposition = response.headers['content-disposition'] ?? `attachment; filename="${req.params.fileId}"`;
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.setHeader('Content-Disposition', contentDisposition);
|
||||
if (response.headers['content-length']) {
|
||||
res.setHeader('Content-Length', response.headers['content-length']);
|
||||
}
|
||||
response.data.pipe(res);
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'NEXTCLOUD_AUTH_INVALID') {
|
||||
await userService.clearNextcloudCredentials(req.user!.id);
|
||||
res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' });
|
||||
return;
|
||||
}
|
||||
logger.error('downloadFile error', { error });
|
||||
res.status(500).json({ success: false, message: 'Datei konnte nicht heruntergeladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async getFilePreview(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const credentials = await userService.getNextcloudCredentials(req.user!.id);
|
||||
if (!credentials) {
|
||||
res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' });
|
||||
return;
|
||||
}
|
||||
const fileId = parseInt(req.params.fileId as string, 10);
|
||||
if (isNaN(fileId)) {
|
||||
res.status(400).json({ success: false, message: 'Ungültige Datei-ID' });
|
||||
return;
|
||||
}
|
||||
const w = parseInt((req.query.w as string) ?? '400', 10) || 400;
|
||||
const h = parseInt((req.query.h as string) ?? '400', 10) || 400;
|
||||
const response = await nextcloudService.getFilePreview(
|
||||
fileId,
|
||||
Math.min(w, 1200),
|
||||
Math.min(h, 1200),
|
||||
credentials.loginName,
|
||||
credentials.appPassword,
|
||||
);
|
||||
const contentType = response.headers['content-type'] ?? 'image/jpeg';
|
||||
res.setHeader('Content-Type', contentType);
|
||||
if (response.headers['content-length']) {
|
||||
res.setHeader('Content-Length', response.headers['content-length']);
|
||||
}
|
||||
res.setHeader('Cache-Control', 'private, max-age=300');
|
||||
response.data.pipe(res);
|
||||
} catch (error: any) {
|
||||
if (error?.code === 'NEXTCLOUD_AUTH_INVALID') {
|
||||
await userService.clearNextcloudCredentials(req.user!.id);
|
||||
res.status(401).json({ success: false, message: 'Nextcloud nicht verbunden' });
|
||||
return;
|
||||
}
|
||||
logger.error('getFilePreview error', { error });
|
||||
res.status(500).json({ success: false, message: 'Vorschau konnte nicht geladen werden' });
|
||||
}
|
||||
}
|
||||
|
||||
async markRoomAsRead(req: Request, res: Response): Promise<void> {
|
||||
try {
|
||||
const credentials = await userService.getNextcloudCredentials(req.user!.id);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Router } from 'express';
|
||||
import multer from 'multer';
|
||||
import nextcloudController from '../controllers/nextcloud.controller';
|
||||
import { authenticate } from '../middleware/auth.middleware';
|
||||
|
||||
const router = Router();
|
||||
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 50 * 1024 * 1024 } });
|
||||
|
||||
router.get('/', authenticate, nextcloudController.getConversations.bind(nextcloudController));
|
||||
router.post('/connect', authenticate, nextcloudController.initiateConnect.bind(nextcloudController));
|
||||
@@ -12,6 +14,10 @@ router.delete('/connect', authenticate, nextcloudController.disconnect.bind(next
|
||||
router.get('/rooms', authenticate, nextcloudController.getRooms.bind(nextcloudController));
|
||||
router.get('/rooms/:token/messages', authenticate, nextcloudController.getMessages.bind(nextcloudController));
|
||||
router.post('/rooms/:token/messages', authenticate, nextcloudController.sendMessage.bind(nextcloudController));
|
||||
router.post('/rooms/:token/files', authenticate, upload.single('file'), nextcloudController.uploadFile.bind(nextcloudController));
|
||||
router.post('/rooms/:token/read', authenticate, nextcloudController.markRoomAsRead.bind(nextcloudController));
|
||||
|
||||
router.get('/files/:fileId/download', authenticate, nextcloudController.downloadFile.bind(nextcloudController));
|
||||
router.get('/files/:fileId/preview', authenticate, nextcloudController.getFilePreview.bind(nextcloudController));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -138,6 +138,7 @@ interface NextcloudChatMessage {
|
||||
timestamp: number;
|
||||
messageType: string;
|
||||
systemMessage: string;
|
||||
messageParameters: Record<string, any>;
|
||||
}
|
||||
|
||||
async function getAllConversations(loginName: string, appPassword: string): Promise<NextcloudConversation[]> {
|
||||
@@ -253,6 +254,7 @@ async function getMessages(token: string, loginName: string, appPassword: string
|
||||
timestamp: m.timestamp,
|
||||
messageType: m.messageType ?? '',
|
||||
systemMessage: m.systemMessage ?? '',
|
||||
messageParameters: m.messageParameters ?? {},
|
||||
}));
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
@@ -412,5 +414,151 @@ async function getConversations(loginName: string, appPassword: string): Promise
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadFileToTalk(
|
||||
token: string,
|
||||
fileBuffer: Buffer,
|
||||
filename: string,
|
||||
mimetype: string,
|
||||
loginName: string,
|
||||
appPassword: string,
|
||||
): Promise<void> {
|
||||
const baseUrl = environment.nextcloudUrl;
|
||||
if (!baseUrl || !isValidServiceUrl(baseUrl)) {
|
||||
throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL');
|
||||
}
|
||||
|
||||
const authHeader = `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`;
|
||||
|
||||
// Add timestamp to avoid filename conflicts
|
||||
const ext = filename.includes('.') ? filename.slice(filename.lastIndexOf('.')) : '';
|
||||
const base = filename.includes('.') ? filename.slice(0, filename.lastIndexOf('.')) : filename;
|
||||
const safeName = `${base}_${Date.now()}${ext}`;
|
||||
const remotePath = `/Talk/${safeName}`;
|
||||
|
||||
try {
|
||||
// Step 1: Upload via WebDAV
|
||||
await httpClient.put(
|
||||
`${baseUrl}/remote.php/dav/files/${encodeURIComponent(loginName)}${remotePath}`,
|
||||
fileBuffer,
|
||||
{
|
||||
headers: {
|
||||
'Authorization': authHeader,
|
||||
'Content-Type': mimetype,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Step 2: Share file to room
|
||||
await httpClient.post(
|
||||
`${baseUrl}/ocs/v2.php/apps/spreed/api/v1/chat/${encodeURIComponent(token)}/share`,
|
||||
{ path: remotePath, shareType: 10 },
|
||||
{
|
||||
headers: {
|
||||
'Authorization': authHeader,
|
||||
'OCS-APIRequest': 'true',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
const err = new Error('Nextcloud authentication invalid');
|
||||
(err as any).code = 'NEXTCLOUD_AUTH_INVALID';
|
||||
throw err;
|
||||
}
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error('NextcloudService.uploadFileToTalk failed', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
});
|
||||
throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`);
|
||||
}
|
||||
logger.error('NextcloudService.uploadFileToTalk failed', { error });
|
||||
throw new Error('Failed to upload file to Nextcloud Talk');
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(
|
||||
filePath: string,
|
||||
loginName: string,
|
||||
appPassword: string,
|
||||
): Promise<any> {
|
||||
const baseUrl = environment.nextcloudUrl;
|
||||
if (!baseUrl || !isValidServiceUrl(baseUrl)) {
|
||||
throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL');
|
||||
}
|
||||
|
||||
const authHeader = `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`;
|
||||
|
||||
try {
|
||||
const response = await httpClient.get(
|
||||
`${baseUrl}/remote.php/dav/files/${encodeURIComponent(loginName)}/${filePath.replace(/^\//, '')}`,
|
||||
{
|
||||
headers: { 'Authorization': authHeader },
|
||||
responseType: 'stream',
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
const err = new Error('Nextcloud authentication invalid');
|
||||
(err as any).code = 'NEXTCLOUD_AUTH_INVALID';
|
||||
throw err;
|
||||
}
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error('NextcloudService.downloadFile failed', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
});
|
||||
throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`);
|
||||
}
|
||||
logger.error('NextcloudService.downloadFile failed', { error });
|
||||
throw new Error('Failed to download file from Nextcloud');
|
||||
}
|
||||
}
|
||||
|
||||
async function getFilePreview(
|
||||
fileId: number,
|
||||
width: number,
|
||||
height: number,
|
||||
loginName: string,
|
||||
appPassword: string,
|
||||
): Promise<any> {
|
||||
const baseUrl = environment.nextcloudUrl;
|
||||
if (!baseUrl || !isValidServiceUrl(baseUrl)) {
|
||||
throw new Error('NEXTCLOUD_URL is not configured or is not a valid service URL');
|
||||
}
|
||||
|
||||
const authHeader = `Basic ${Buffer.from(loginName + ':' + appPassword).toString('base64')}`;
|
||||
|
||||
try {
|
||||
const response = await httpClient.get(
|
||||
`${baseUrl}/index.php/core/preview`,
|
||||
{
|
||||
params: { fileId, x: width, y: height, a: 1 },
|
||||
headers: { 'Authorization': authHeader },
|
||||
responseType: 'stream',
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 401) {
|
||||
const err = new Error('Nextcloud authentication invalid');
|
||||
(err as any).code = 'NEXTCLOUD_AUTH_INVALID';
|
||||
throw err;
|
||||
}
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error('NextcloudService.getFilePreview failed', {
|
||||
status: error.response?.status,
|
||||
statusText: error.response?.statusText,
|
||||
});
|
||||
throw new Error(`Nextcloud API error: ${error.response?.status ?? 'unknown'}`);
|
||||
}
|
||||
logger.error('NextcloudService.getFilePreview failed', { error });
|
||||
throw new Error('Failed to get file preview from Nextcloud');
|
||||
}
|
||||
}
|
||||
|
||||
export type { NextcloudChatMessage, GetMessagesOptions };
|
||||
export default { initiateLoginFlow, pollLoginFlow, getConversations, getAllConversations, getMessages, sendMessage, markAsRead };
|
||||
export default { initiateLoginFlow, pollLoginFlow, getConversations, getAllConversations, getMessages, sendMessage, markAsRead, uploadFileToTalk, downloadFile, getFilePreview };
|
||||
|
||||
51
backend/src/types/multer.d.ts
vendored
Normal file
51
backend/src/types/multer.d.ts
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
// Minimal type declaration for multer — replaced by @types/multer after npm install
|
||||
declare module 'multer' {
|
||||
import { RequestHandler } from 'express';
|
||||
|
||||
interface StorageEngine {}
|
||||
|
||||
interface Options {
|
||||
storage?: StorageEngine;
|
||||
limits?: {
|
||||
fileSize?: number;
|
||||
files?: number;
|
||||
fields?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Multer {
|
||||
single(fieldname: string): RequestHandler;
|
||||
array(fieldname: string, maxCount?: number): RequestHandler;
|
||||
none(): RequestHandler;
|
||||
}
|
||||
|
||||
interface MulterStatic {
|
||||
(options?: Options): Multer;
|
||||
memoryStorage(): StorageEngine;
|
||||
diskStorage(options: any): StorageEngine;
|
||||
}
|
||||
|
||||
const multer: MulterStatic;
|
||||
export default multer;
|
||||
}
|
||||
|
||||
declare namespace Express {
|
||||
interface Request {
|
||||
file?: {
|
||||
fieldname: string;
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
buffer: Buffer;
|
||||
};
|
||||
files?: {
|
||||
fieldname: string;
|
||||
originalname: string;
|
||||
encoding: string;
|
||||
mimetype: string;
|
||||
size: number;
|
||||
buffer: Buffer;
|
||||
}[];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user