markdown-notes-engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +196 -0
- package/lib/README.md +312 -0
- package/lib/backend/github.js +318 -0
- package/lib/backend/index.js +76 -0
- package/lib/backend/markdown.js +62 -0
- package/lib/backend/routes/notes.js +197 -0
- package/lib/backend/routes/search.js +28 -0
- package/lib/backend/routes/upload.js +122 -0
- package/lib/backend/storage.js +121 -0
- package/lib/frontend/index.js +665 -0
- package/lib/frontend/styles.css +431 -0
- package/lib/index.js +28 -0
- package/package.json +51 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Routes
|
|
3
|
+
* Handles image and video uploads
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const express = require('express');
|
|
7
|
+
const fileUpload = require('express-fileupload');
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
|
|
10
|
+
// File upload middleware
|
|
11
|
+
router.use(fileUpload());
|
|
12
|
+
|
|
13
|
+
// Upload image (multipart form)
|
|
14
|
+
router.post('/upload-image', async (req, res) => {
|
|
15
|
+
try {
|
|
16
|
+
if (!req.files || !req.files.image) {
|
|
17
|
+
return res.status(400).json({ error: 'No image file provided' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const imageFile = req.files.image;
|
|
21
|
+
const folder = req.body.folder || '';
|
|
22
|
+
|
|
23
|
+
const { storageClient } = req.notesEngine;
|
|
24
|
+
const imageUrl = await storageClient.uploadImage(
|
|
25
|
+
imageFile.data,
|
|
26
|
+
imageFile.name,
|
|
27
|
+
folder
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
res.json({ imageUrl });
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error('Error uploading image:', error);
|
|
33
|
+
res.status(500).json({ error: 'Failed to upload image' });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Upload image (base64)
|
|
38
|
+
router.post('/upload-image-base64', async (req, res) => {
|
|
39
|
+
try {
|
|
40
|
+
const { image, filename, folder = '' } = req.body;
|
|
41
|
+
|
|
42
|
+
if (!image || !filename) {
|
|
43
|
+
return res.status(400).json({ error: 'Image data and filename are required' });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Remove data URL prefix if present
|
|
47
|
+
const base64Data = image.replace(/^data:image\/\w+;base64,/, '');
|
|
48
|
+
const buffer = Buffer.from(base64Data, 'base64');
|
|
49
|
+
|
|
50
|
+
const { storageClient } = req.notesEngine;
|
|
51
|
+
const imageUrl = await storageClient.uploadImage(buffer, filename, folder);
|
|
52
|
+
|
|
53
|
+
res.json({ imageUrl });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Error uploading image:', error);
|
|
56
|
+
res.status(500).json({ error: 'Failed to upload image' });
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Upload video
|
|
61
|
+
router.post('/upload-video', async (req, res) => {
|
|
62
|
+
try {
|
|
63
|
+
if (!req.files || !req.files.video) {
|
|
64
|
+
return res.status(400).json({ error: 'No video file provided' });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const videoFile = req.files.video;
|
|
68
|
+
const folder = req.body.folder || '';
|
|
69
|
+
|
|
70
|
+
const { storageClient } = req.notesEngine;
|
|
71
|
+
const videoUrl = await storageClient.uploadVideo(
|
|
72
|
+
videoFile.data,
|
|
73
|
+
videoFile.name,
|
|
74
|
+
folder
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
res.json({ videoUrl });
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('Error uploading video:', error);
|
|
80
|
+
res.status(500).json({ error: 'Failed to upload video' });
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Delete image
|
|
85
|
+
router.delete('/image', async (req, res) => {
|
|
86
|
+
try {
|
|
87
|
+
const { imageUrl } = req.body;
|
|
88
|
+
|
|
89
|
+
if (!imageUrl) {
|
|
90
|
+
return res.status(400).json({ error: 'Image URL is required' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { storageClient } = req.notesEngine;
|
|
94
|
+
await storageClient.deleteFile(imageUrl);
|
|
95
|
+
|
|
96
|
+
res.json({ success: true });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Error deleting image:', error);
|
|
99
|
+
res.status(500).json({ error: 'Failed to delete image' });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Delete video
|
|
104
|
+
router.delete('/video', async (req, res) => {
|
|
105
|
+
try {
|
|
106
|
+
const { videoUrl } = req.body;
|
|
107
|
+
|
|
108
|
+
if (!videoUrl) {
|
|
109
|
+
return res.status(400).json({ error: 'Video URL is required' });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const { storageClient } = req.notesEngine;
|
|
113
|
+
await storageClient.deleteFile(videoUrl);
|
|
114
|
+
|
|
115
|
+
res.json({ success: true });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Error deleting video:', error);
|
|
118
|
+
res.status(500).json({ error: 'Failed to delete video' });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
module.exports = router;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Client
|
|
3
|
+
* Handles file uploads to R2 or S3
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { S3Client, PutObjectCommand, DeleteObjectCommand } = require('@aws-sdk/client-s3');
|
|
7
|
+
|
|
8
|
+
class StorageClient {
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.publicUrl = config.publicUrl;
|
|
12
|
+
|
|
13
|
+
const s3Config = {
|
|
14
|
+
region: config.type === 'r2' ? 'auto' : (config.region || 'us-east-1'),
|
|
15
|
+
credentials: {
|
|
16
|
+
accessKeyId: config.accessKeyId,
|
|
17
|
+
secretAccessKey: config.secretAccessKey
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// R2-specific endpoint
|
|
22
|
+
if (config.type === 'r2') {
|
|
23
|
+
s3Config.endpoint = `https://${config.accountId}.r2.cloudflarestorage.com`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
this.s3Client = new S3Client(s3Config);
|
|
27
|
+
this.bucketName = config.bucketName;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Upload an image file
|
|
32
|
+
* @param {Buffer} fileBuffer - File buffer
|
|
33
|
+
* @param {string} filename - Original filename
|
|
34
|
+
* @param {string} [folder=''] - Optional folder path
|
|
35
|
+
* @returns {Promise<string>} Public URL of uploaded file
|
|
36
|
+
*/
|
|
37
|
+
async uploadImage(fileBuffer, filename, folder = '') {
|
|
38
|
+
const timestamp = Date.now();
|
|
39
|
+
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
40
|
+
const key = folder
|
|
41
|
+
? `images/${folder}/${timestamp}-${sanitizedFilename}`
|
|
42
|
+
: `images/${timestamp}-${sanitizedFilename}`;
|
|
43
|
+
|
|
44
|
+
await this.s3Client.send(new PutObjectCommand({
|
|
45
|
+
Bucket: this.bucketName,
|
|
46
|
+
Key: key,
|
|
47
|
+
Body: fileBuffer,
|
|
48
|
+
ContentType: this._getContentType(filename)
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
return `${this.publicUrl}/${key}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Upload a video file
|
|
56
|
+
* @param {Buffer} fileBuffer - File buffer
|
|
57
|
+
* @param {string} filename - Original filename
|
|
58
|
+
* @param {string} [folder=''] - Optional folder path
|
|
59
|
+
* @returns {Promise<string>} Public URL of uploaded file
|
|
60
|
+
*/
|
|
61
|
+
async uploadVideo(fileBuffer, filename, folder = '') {
|
|
62
|
+
const timestamp = Date.now();
|
|
63
|
+
const sanitizedFilename = filename.replace(/[^a-zA-Z0-9.-]/g, '_');
|
|
64
|
+
const key = folder
|
|
65
|
+
? `videos/${folder}/${timestamp}-${sanitizedFilename}`
|
|
66
|
+
: `videos/${timestamp}-${sanitizedFilename}`;
|
|
67
|
+
|
|
68
|
+
await this.s3Client.send(new PutObjectCommand({
|
|
69
|
+
Bucket: this.bucketName,
|
|
70
|
+
Key: key,
|
|
71
|
+
Body: fileBuffer,
|
|
72
|
+
ContentType: this._getContentType(filename)
|
|
73
|
+
}));
|
|
74
|
+
|
|
75
|
+
return `${this.publicUrl}/${key}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delete a file
|
|
80
|
+
* @param {string} fileUrl - Public URL of the file
|
|
81
|
+
* @returns {Promise<void>}
|
|
82
|
+
*/
|
|
83
|
+
async deleteFile(fileUrl) {
|
|
84
|
+
// Extract key from URL
|
|
85
|
+
const key = fileUrl.replace(this.publicUrl + '/', '');
|
|
86
|
+
|
|
87
|
+
await this.s3Client.send(new DeleteObjectCommand({
|
|
88
|
+
Bucket: this.bucketName,
|
|
89
|
+
Key: key
|
|
90
|
+
}));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get content type from filename
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
_getContentType(filename) {
|
|
98
|
+
const ext = filename.split('.').pop().toLowerCase();
|
|
99
|
+
|
|
100
|
+
const contentTypes = {
|
|
101
|
+
// Images
|
|
102
|
+
jpg: 'image/jpeg',
|
|
103
|
+
jpeg: 'image/jpeg',
|
|
104
|
+
png: 'image/png',
|
|
105
|
+
gif: 'image/gif',
|
|
106
|
+
webp: 'image/webp',
|
|
107
|
+
svg: 'image/svg+xml',
|
|
108
|
+
|
|
109
|
+
// Videos
|
|
110
|
+
mp4: 'video/mp4',
|
|
111
|
+
webm: 'video/webm',
|
|
112
|
+
mov: 'video/quicktime',
|
|
113
|
+
avi: 'video/x-msvideo',
|
|
114
|
+
mkv: 'video/x-matroska'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return contentTypes[ext] || 'application/octet-stream';
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { StorageClient };
|