1212use OCA \Pantry \ResponseDefinitions ;
1313use OCA \Pantry \Service \CategoryService ;
1414use OCA \Pantry \Service \HouseAuthService ;
15+ use OCA \Pantry \Service \ImageService ;
1516use OCA \Pantry \Service \ShoppingListService ;
1617use OCP \AppFramework \Http ;
1718use OCP \AppFramework \Http \Attribute \ApiRoute ;
@@ -35,6 +36,7 @@ public function __construct(
3536 private ShoppingListService $ lists ,
3637 private CategoryService $ categories ,
3738 private HouseAuthService $ auth ,
39+ private ImageService $ images ,
3840 private IUserSession $ userSession ,
3941 ) {
4042 parent ::__construct ($ appName , $ request );
@@ -248,6 +250,7 @@ public function addItem(
248250 * @param string|null $quantity New quantity (empty string clears).
249251 * @param string|null $rrule New RRULE (empty string clears).
250252 * @param bool|null $repeatFromCompletion New recurrence anchor mode.
253+ * @param int|null $imageFileId File id of attached image (0 or negative clears).
251254 * @param int|null $sortOrder New sort order.
252255 *
253256 * @return DataResponse<Http::STATUS_OK, PantryListItem, array{}>
@@ -265,9 +268,10 @@ public function updateItem(
265268 ?string $ quantity = null ,
266269 ?string $ rrule = null ,
267270 ?bool $ repeatFromCompletion = null ,
271+ ?int $ imageFileId = null ,
268272 ?int $ sortOrder = null ,
269273 ): DataResponse {
270- return $ this ->runAction (function () use ($ houseId , $ listId , $ itemId , $ name , $ categoryId , $ quantity , $ rrule , $ repeatFromCompletion , $ sortOrder ): DataResponse {
274+ return $ this ->runAction (function () use ($ houseId , $ listId , $ itemId , $ name , $ categoryId , $ quantity , $ rrule , $ repeatFromCompletion , $ imageFileId , $ sortOrder ): DataResponse {
271275 $ this ->auth ->requireMember ($ houseId , $ this ->requireUid ());
272276 $ item = $ this ->lists ->getItem ($ itemId );
273277 $ list = $ this ->lists ->getList ($ item ->getListId ());
@@ -296,6 +300,9 @@ public function updateItem(
296300 if ($ repeatFromCompletion !== null ) {
297301 $ patch ['repeatFromCompletion ' ] = $ repeatFromCompletion ;
298302 }
303+ if ($ imageFileId !== null ) {
304+ $ patch ['imageFileId ' ] = $ imageFileId > 0 ? $ imageFileId : null ;
305+ }
299306 if ($ sortOrder !== null ) {
300307 $ patch ['sortOrder ' ] = $ sortOrder ;
301308 }
@@ -359,6 +366,81 @@ public function deleteItem(int $houseId, int $listId, int $itemId): DataResponse
359366 });
360367 }
361368
369+ /**
370+ * Upload an image for an item
371+ *
372+ * Uploads the request body as an image into the user's configured pantry
373+ * image folder and attaches it to the item.
374+ *
375+ * @param int $houseId House id.
376+ * @param int $listId List id.
377+ * @param int $itemId Item id.
378+ *
379+ * @return DataResponse<Http::STATUS_OK, PantryListItem, array{}>
380+ *
381+ * 200: Image uploaded and attached
382+ */
383+ #[ApiRoute(verb: 'POST ' , url: '/api/houses/{houseId}/lists/{listId}/items/{itemId}/image ' )]
384+ #[NoAdminRequired]
385+ public function uploadItemImage (int $ houseId , int $ listId , int $ itemId ): DataResponse {
386+ return $ this ->runAction (function () use ($ houseId , $ listId , $ itemId ): DataResponse {
387+ $ uid = $ this ->requireUid ();
388+ $ this ->auth ->requireMember ($ houseId , $ uid );
389+ $ item = $ this ->lists ->getItem ($ itemId );
390+ $ list = $ this ->lists ->getList ($ item ->getListId ());
391+ $ this ->assertListInHouse ($ list ->getHouseId (), $ houseId );
392+ if ($ item ->getListId () !== $ listId ) {
393+ throw new NotFoundException ('Item does not belong to this list ' );
394+ }
395+
396+ $ data = $ this ->request ->getUploadedFile ('image ' );
397+ if ($ data === null || !is_array ($ data ) || ($ data ['error ' ] ?? UPLOAD_ERR_NO_FILE ) !== UPLOAD_ERR_OK ) {
398+ throw new \InvalidArgumentException ('No image uploaded ' );
399+ }
400+ $ tmp = (string )($ data ['tmp_name ' ] ?? '' );
401+ if ($ tmp === '' || !is_uploaded_file ($ tmp )) {
402+ throw new \InvalidArgumentException ('Invalid upload ' );
403+ }
404+ $ bytes = file_get_contents ($ tmp );
405+ if ($ bytes === false ) {
406+ throw new \RuntimeException ('Could not read uploaded file ' );
407+ }
408+ $ original = (string )($ data ['name ' ] ?? 'image.jpg ' );
409+ $ fileId = $ this ->images ->uploadForUser ($ uid , $ original , $ bytes );
410+
411+ $ updated = $ this ->lists ->updateItem ($ itemId , ['imageFileId ' => $ fileId ]);
412+ return new DataResponse ($ updated ->jsonSerialize ());
413+ });
414+ }
415+
416+ /**
417+ * Clear the image attached to an item
418+ *
419+ * @param int $houseId House id.
420+ * @param int $listId List id.
421+ * @param int $itemId Item id.
422+ *
423+ * @return DataResponse<Http::STATUS_OK, PantryListItem, array{}>
424+ *
425+ * 200: Image cleared
426+ */
427+ #[ApiRoute(verb: 'DELETE ' , url: '/api/houses/{houseId}/lists/{listId}/items/{itemId}/image ' )]
428+ #[NoAdminRequired]
429+ public function clearItemImage (int $ houseId , int $ listId , int $ itemId ): DataResponse {
430+ return $ this ->runAction (function () use ($ houseId , $ listId , $ itemId ): DataResponse {
431+ $ uid = $ this ->requireUid ();
432+ $ this ->auth ->requireMember ($ houseId , $ uid );
433+ $ item = $ this ->lists ->getItem ($ itemId );
434+ $ list = $ this ->lists ->getList ($ item ->getListId ());
435+ $ this ->assertListInHouse ($ list ->getHouseId (), $ houseId );
436+ if ($ item ->getListId () !== $ listId ) {
437+ throw new NotFoundException ('Item does not belong to this list ' );
438+ }
439+ $ updated = $ this ->lists ->updateItem ($ itemId , ['imageFileId ' => null ]);
440+ return new DataResponse ($ updated ->jsonSerialize ());
441+ });
442+ }
443+
362444 private function requireUid (): string {
363445 $ user = $ this ->userSession ->getUser ();
364446 if ($ user === null ) {
0 commit comments