Skip to content

Commit a4d6891

Browse files
committed
feat: improve attachment storage for API
- Attachments are stored in note-specific folder - Attachment filenames are retained This mirrors the behaviour for attachments created in the web UI.
1 parent 3b66580 commit a4d6891

File tree

3 files changed

+82
-24
lines changed

3 files changed

+82
-24
lines changed

appinfo/routes.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@
200200
'url' => '/api/{apiVersion}/attachment/{noteid}',
201201
'verb' => 'GET',
202202
'requirements' => [
203-
'apiVersion' => '(v1.4)',
203+
'apiVersion' => '(v1|v1.4)',
204204
'noteid' => '\d+'
205205
],
206206
],
@@ -209,7 +209,7 @@
209209
'url' => '/api/{apiVersion}/attachment/{noteid}',
210210
'verb' => 'POST',
211211
'requirements' => [
212-
'apiVersion' => '(v1.4)',
212+
'apiVersion' => '(v1|v1.4)',
213213
'noteid' => '\d+'
214214
],
215215
],

docs/api/v1.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ In this document, the Notes API major version 1 and all its minor versions are d
1515
| **1.1** | Notes 3.4 (May 2020) | Filter "Get all notes" by category |
1616
| **1.2** | Notes 4.1 (June 2021) | Preventing lost updates, read-only notes, settings |
1717
| **1.3** | Notes 4.5 (August 2022) | Allow custom file suffixes |
18-
| **1.4** | Notes 4.9 (August 2025) | Add external image api |
18+
| **1.4** | Notes 4.9 (August 2025) | Add external image API |
1919

2020

2121

@@ -288,7 +288,7 @@ No valid authentication credentials supplied.
288288
| Parameter | Type | Description |
289289
|:----------|:-----------------------------|:-------------------------------------------|
290290
| `id` | integer, required (path) | ID of the note to load the attachment from |
291-
| `path` | string, required (request) | Path or name of the attachment to load. |
291+
| `path` | string, required (request) | Path of the attachment to load |
292292

293293
Example:
294294

@@ -327,16 +327,16 @@ curl -u "user:password" \
327327
-F "file=@/path/to/image.png" \
328328
"https://yournextcloud.com/index.php/apps/notes/api/v1.4/attachment/<id>"
329329

330-
# The post request will return the filename that was generated:
331-
{"filename":"d8aef2005b4f815fec8ade5388240f2c.png"}
330+
# The post request will return the path where the image is stored:
331+
{"filename":".attachments.<id>/image.png"}
332332
```
333333

334334
#### Response
335335
##### 200 OK
336-
- **Body**: Filename in json encoded:
336+
- **Body**: Path in JSON encoded, example:
337337
```js
338338
{
339-
"filename": "image.jpg"
339+
"filaname": ".attachments.1234/image.png"
340340
}
341341
```
342342

lib/Service/NotesService.php

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,26 @@
1313
use OCP\Files\File;
1414
use OCP\Files\FileInfo;
1515
use OCP\Files\Folder;
16+
use OCP\Files\IFilenameValidator;
17+
use OCP\Files\NotFoundException;
1618
use OCP\Files\NotPermittedException;
1719

1820
class NotesService {
1921
private MetaService $metaService;
2022
private SettingsService $settings;
2123
private NoteUtil $noteUtil;
24+
private IFilenameValidator $filenameValidator;
2225

2326
public function __construct(
2427
MetaService $metaService,
2528
SettingsService $settings,
2629
NoteUtil $noteUtil,
30+
IFilenameValidator $filenameValidator,
2731
) {
2832
$this->metaService = $metaService;
2933
$this->settings = $settings;
3034
$this->noteUtil = $noteUtil;
35+
$this->filenameValidator = $filenameValidator;
3136
}
3237

3338
public function getAll(string $userId, bool $autoCreateNotesFolder = false) : array {
@@ -223,8 +228,8 @@ private static function getFileById(string $customExtension, Folder $folder, int
223228
* @NoCSRFRequired
224229
* @return \OCP\Files\File
225230
*/
226-
public function getAttachment(string $userId, int $noteid, string $path) : File {
227-
$note = $this->get($userId, $noteid);
231+
public function getAttachment(string $userId, int $noteId, string $path) : File {
232+
$note = $this->get($userId, $noteId);
228233
$notesFolder = $this->getNotesFolder($userId);
229234
$path = str_replace('\\', '/', $path); // change windows style path
230235
$p = explode('/', $note->getCategory());
@@ -243,24 +248,24 @@ public function getAttachment(string $userId, int $noteid, string $path) : File
243248

244249
/**
245250
* @param $userId
246-
* @param $noteid
251+
* @param $noteId
247252
* @param $fileDataArray
253+
*
254+
* @return array
248255
* @throws NotPermittedException
249256
* @throws ImageNotWritableException
250-
* https://github.com/nextcloud/deck/blob/master/lib/Service/AttachmentService.php
257+
* @throws NotFoundException
258+
* @throws InvalidPathException
259+
* https://github.com/nextcloud/text/blob/main/lib/Service/AttachmentService.php
251260
*/
252-
public function createImage(string $userId, int $noteid, $fileDataArray) {
253-
$note = $this->get($userId, $noteid);
261+
public function createImage(string $userId, int $noteId, $fileDataArray) : array {
262+
$note = $this->get($userId, $noteId);
254263
$notesFolder = $this->getNotesFolder($userId);
255-
$parent = $this->noteUtil->getCategoryFolder($notesFolder, $note->getCategory());
264+
$parentFolder = $this->noteUtil->getCategoryFolder($notesFolder, $note->getCategory());
256265

257-
// try to generate long id, if not available on system fall back to a shorter one
258-
try {
259-
$filename = bin2hex(random_bytes(16));
260-
} catch (\Exception $e) {
261-
$filename = uniqid();
262-
}
263-
$filename = $filename . '.' . explode('.', $fileDataArray['name'])[1];
266+
$saveDir = $this->getAttachmentDirectoryForNote($note, $userId);
267+
$fileName = self::getUniqueFileName($saveDir, $fileDataArray['name']);
268+
$this->filenameValidator->validateFilename($fileName);
264269

265270
if ($fileDataArray['tmp_name'] === '') {
266271
throw new ImageNotWritableException();
@@ -272,8 +277,61 @@ public function createImage(string $userId, int $noteid, $fileDataArray) {
272277
fclose($fp);
273278

274279
$result = [];
275-
$result['filename'] = $filename;
276-
$this->noteUtil->getRoot()->newFile($parent->getPath() . '/' . $filename, $content);
280+
$result['filename'] = '.attachments.' . $note->getId() . '/' . $fileName;
281+
$saveDir->newFile($fileName, $content);
277282
return $result;
278283
}
284+
285+
/**
286+
* Get unique file name in a directory. Add '(n)' suffix.
287+
*
288+
* @param Folder $dir
289+
* @param string $fileName
290+
*
291+
* @return string
292+
*/
293+
public static function getUniqueFileName(Folder $dir, string $fileName) : string {
294+
$extension = pathinfo($fileName, PATHINFO_EXTENSION);
295+
$counter = 1;
296+
$uniqueFileName = $fileName;
297+
if ($extension !== '') {
298+
while ($dir->nodeExists($uniqueFileName)) {
299+
$counter++;
300+
$uniqueFileName = (string)preg_replace('/\.' . $extension . '$/', ' (' . $counter . ').' . $extension, $fileName);
301+
}
302+
} else {
303+
while ($dir->nodeExists($uniqueFileName)) {
304+
$counter++;
305+
$uniqueFileName = (string)preg_replace('/$/', ' (' . $counter . ')', $fileName);
306+
}
307+
}
308+
return $uniqueFileName;
309+
}
310+
311+
/**
312+
* Get or create file--specific attachment folder
313+
*
314+
* @param Note $note
315+
* @param string $userid
316+
*
317+
* @return Folder
318+
* @throws NotFoundException
319+
* @throws NotPermittedException
320+
* @throws InvalidPathException
321+
*/
322+
private function getAttachmentDirectoryForNote(Note $note, string $userId) : Folder {
323+
$notesFolder = $this->getNotesFolder($userId);
324+
$parentFolder = $this->noteUtil->getCategoryFolder($notesFolder, $note->getCategory());
325+
326+
$attachmentFolderName = '.attachments.' . $note->getId();
327+
if ($parentFolder->nodeExists($attachmentFolderName)) {
328+
$attachmentFolder = $parentFolder->get($attachmentFolderName);
329+
if ($attachmentFolder instanceof Folder) {
330+
return $attachmentFolder;
331+
}
332+
} else {
333+
return $parentFolder->newFolder($attachmentFolderName);
334+
}
335+
throw new NotFoundException('Attachment dir for note ' . $note->getId() . ' was not found or could not be created.');
336+
}
279337
}

0 commit comments

Comments
 (0)