buddy-workbench 0.1.98 → 0.1.100
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/package.json +1 -1
- package/server/config.js +1 -0
- package/server/repositories/memos.js +48 -2
- package/server/routes/memos.js +36 -2
- package/ui/dist/assets/{index-BOaZJPf3.css → index-CF6qAvVa.css} +1 -1
- package/ui/dist/assets/{index-D0UqZsgN.js → index-lYdgCAaq.js} +168 -162
- package/ui/dist/assets/{xmindAdapter-CijBAr6U.js → xmindAdapter-zQJewFBX.js} +1 -1
- package/ui/dist/index.html +2 -2
- package/ui/dist/sw.js +2 -2
package/package.json
CHANGED
package/server/config.js
CHANGED
|
@@ -30,6 +30,7 @@ export const paths = {
|
|
|
30
30
|
jiraRecentlyCreated: join(userDataDir, 'jira-recently-created.json'),
|
|
31
31
|
todos: join(userDataDir, 'todos.json'),
|
|
32
32
|
memos: join(userDataDir, 'memos.json'),
|
|
33
|
+
memoImages: join(userDataDir, 'memo-images'),
|
|
33
34
|
staticPages: join(userDataDir, 'static-pages.json'),
|
|
34
35
|
errors: join(userDataDir, 'errors.json'),
|
|
35
36
|
postman: join(userDataDir, 'postman.json'),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
3
4
|
import { paths } from '../config.js';
|
|
4
5
|
|
|
5
6
|
export const starterMemos = [
|
|
@@ -81,3 +82,48 @@ export function saveMemos(memos) {
|
|
|
81
82
|
mkdirSync(dirname(paths.memos), { recursive: true });
|
|
82
83
|
writeFileSync(paths.memos, JSON.stringify(memos, null, 2));
|
|
83
84
|
}
|
|
85
|
+
|
|
86
|
+
const imageExtensions = {
|
|
87
|
+
'image/gif': 'gif',
|
|
88
|
+
'image/jpeg': 'jpg',
|
|
89
|
+
'image/png': 'png',
|
|
90
|
+
'image/webp': 'webp'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export function saveMemoImage(buffer, contentType) {
|
|
94
|
+
const extension = imageExtensions[contentType];
|
|
95
|
+
if (!extension) return null;
|
|
96
|
+
mkdirSync(paths.memoImages, { recursive: true, mode: 0o700 });
|
|
97
|
+
const filename = `${randomUUID()}.${extension}`;
|
|
98
|
+
writeFileSync(join(paths.memoImages, filename), buffer);
|
|
99
|
+
return { filename, url: `/api/memos/images/${filename}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function memoImagePath(filename) {
|
|
103
|
+
const safeFilename = basename(filename);
|
|
104
|
+
if (safeFilename !== filename || !/^[0-9a-f-]+\.(?:gif|jpg|png|webp)$/i.test(safeFilename)) return null;
|
|
105
|
+
return join(paths.memoImages, safeFilename);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function listMemoImages() {
|
|
109
|
+
if (!existsSync(paths.memoImages)) return [];
|
|
110
|
+
return readdirSync(paths.memoImages)
|
|
111
|
+
.filter((filename) => memoImagePath(filename))
|
|
112
|
+
.map((filename) => {
|
|
113
|
+
const filePath = memoImagePath(filename);
|
|
114
|
+
const stats = statSync(filePath);
|
|
115
|
+
const url = `/api/memos/images/${filename}`;
|
|
116
|
+
const usedBy = listMemos()
|
|
117
|
+
.filter((memo) => String(memo.content || '').includes(url))
|
|
118
|
+
.map((memo) => ({ id: memo.id, title: memo.title || 'Untitled memo' }));
|
|
119
|
+
return { filename, url, size: stats.size, createdAt: stats.birthtime.toISOString(), updatedAt: stats.mtime.toISOString(), usedBy };
|
|
120
|
+
})
|
|
121
|
+
.sort((left, right) => new Date(right.updatedAt) - new Date(left.updatedAt));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function deleteMemoImage(filename) {
|
|
125
|
+
const filePath = memoImagePath(filename);
|
|
126
|
+
if (!filePath || !existsSync(filePath)) return false;
|
|
127
|
+
unlinkSync(filePath);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
package/server/routes/memos.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Router } from 'express';
|
|
2
|
-
import {
|
|
1
|
+
import express, { Router } from 'express';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { deleteMemoImage, listMemoImages, listMemos, memoImagePath, saveMemoImage, saveMemos, starterMemos } from '../repositories/memos.js';
|
|
3
4
|
|
|
4
5
|
const router = Router();
|
|
5
6
|
|
|
@@ -21,6 +22,39 @@ router.get('/', (_req, res) => {
|
|
|
21
22
|
res.json(cleanedMemos);
|
|
22
23
|
});
|
|
23
24
|
|
|
25
|
+
router.get('/images', (_req, res) => res.json(listMemoImages()));
|
|
26
|
+
|
|
27
|
+
router.post('/images', express.raw({ type: 'image/*', limit: '10mb' }), (req, res) => {
|
|
28
|
+
try {
|
|
29
|
+
const contentType = String(req.headers['content-type'] || '').split(';')[0].toLowerCase();
|
|
30
|
+
if (!req.body?.length) return res.status(400).json({ error: 'Image body is empty.' });
|
|
31
|
+
const image = saveMemoImage(req.body, contentType);
|
|
32
|
+
if (!image) return res.status(415).json({ error: 'Only PNG, JPEG, GIF, and WebP images are supported.' });
|
|
33
|
+
res.status(201).json(image);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
res.status(500).json({ error: error.message });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
router.get('/images/:filename', (req, res) => {
|
|
40
|
+
try {
|
|
41
|
+
const filePath = memoImagePath(req.params.filename);
|
|
42
|
+
if (!filePath || !existsSync(filePath)) return res.status(404).json({ error: 'Image not found.' });
|
|
43
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
44
|
+
res.type(filePath).send(readFileSync(filePath));
|
|
45
|
+
} catch {
|
|
46
|
+
res.status(404).json({ error: 'Image not found.' });
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
router.delete('/images/:filename', (req, res) => {
|
|
51
|
+
const image = listMemoImages().find((item) => item.filename === req.params.filename);
|
|
52
|
+
if (!image) return res.status(404).json({ error: 'Image not found.' });
|
|
53
|
+
if (image.usedBy.length) return res.status(409).json({ error: 'This image is still used by a memo.', usedBy: image.usedBy });
|
|
54
|
+
deleteMemoImage(req.params.filename);
|
|
55
|
+
res.status(204).end();
|
|
56
|
+
});
|
|
57
|
+
|
|
24
58
|
router.post('/', (req, res) => {
|
|
25
59
|
const data = validMemo(req.body);
|
|
26
60
|
if (!data) return res.status(400).json({ error: 'Memo content is required.' });
|