fix: proxy images for shared access

This commit is contained in:
2026-04-07 12:41:21 +03:00
parent 6cf31e2df0
commit 674a4d3d82
17 changed files with 574 additions and 94 deletions

View File

@@ -419,7 +419,7 @@ final class ChecklistController extends OCSController {
$original = (string)($data['name'] ?? 'image.jpg');
$fileId = $this->images->uploadForUser($uid, $houseId, $original, $bytes);
$updated = $this->lists->updateItem($itemId, ['imageFileId' => $fileId]);
$updated = $this->lists->updateItem($itemId, ['imageFileId' => $fileId, 'imageUploadedBy' => $uid]);
return new DataResponse($updated->jsonSerialize());
});
}
@@ -447,7 +447,7 @@ final class ChecklistController extends OCSController {
if ($item->getListId() !== $listId) {
throw new NotFoundException('Item does not belong to this list');
}
$updated = $this->lists->updateItem($itemId, ['imageFileId' => null]);
$updated = $this->lists->updateItem($itemId, ['imageFileId' => null, 'imageUploadedBy' => null]);
return new DataResponse($updated->jsonSerialize());
});
}

View File

@@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
// SPDX-FileCopyrightText: Chen Asraf <contact@casraf.dev>
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace OCA\Pantry\Controller;
use OCA\Pantry\Db\PhotoMapper;
use OCA\Pantry\Exception\ForbiddenException;
use OCA\Pantry\Exception\NotFoundException;
use OCA\Pantry\Service\HouseAuthService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\OCSController;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IPreview;
use OCP\IRequest;
use OCP\IUserSession;
/**
* Serve images from the owner's storage so any house member can view them,
* regardless of Nextcloud sharing settings.
*/
final class ImageController extends OCSController {
use TranslatesDomainExceptions;
public function __construct(
string $appName,
IRequest $request,
private HouseAuthService $auth,
private PhotoMapper $photoMapper,
private IRootFolder $rootFolder,
private IPreview $previewManager,
private IUserSession $userSession,
) {
parent::__construct($appName, $request);
}
/**
* Serve a photo board image preview
*
* @param int $houseId House id.
* @param int $photoId Photo record id.
* @param int $size Preview size (longest edge).
*
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: Preview returned
* 404: Image not found
*/
#[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/photos/{photoId}/preview')]
#[NoAdminRequired]
#[NoCSRFRequired]
public function photoPreview(int $houseId, int $photoId, int $size = 300): FileDisplayResponse|DataResponse {
return $this->runAction(function () use ($houseId, $photoId, $size) {
$this->auth->requireMember($houseId, $this->requireUid());
$photo = $this->photoMapper->findById($photoId);
if ($photo->getHouseId() !== $houseId) {
throw new NotFoundException('Photo does not belong to this house');
}
return $this->servePreview($photo->getUploadedBy(), $photo->getFileId(), $size);
});
}
/**
* Serve a checklist item image preview
*
* @param int $houseId House id.
* @param int $fileId Nextcloud file id.
* @param string $owner File owner uid.
* @param int $size Preview size (longest edge).
*
* @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: Preview returned
* 404: Image not found
*/
#[ApiRoute(verb: 'GET', url: '/api/houses/{houseId}/image-preview')]
#[NoAdminRequired]
#[NoCSRFRequired]
public function itemImagePreview(int $houseId, int $fileId, string $owner, int $size = 300): FileDisplayResponse|DataResponse {
return $this->runAction(function () use ($houseId, $fileId, $owner, $size) {
$this->auth->requireMember($houseId, $this->requireUid());
return $this->servePreview($owner, $fileId, $size);
});
}
private function servePreview(string $ownerUid, int $fileId, int $size): FileDisplayResponse|DataResponse {
$size = max(16, min($size, 2048));
$userFolder = $this->rootFolder->getUserFolder($ownerUid);
$nodes = $userFolder->getById($fileId);
if (empty($nodes)) {
return new DataResponse(['error' => 'File not found'], Http::STATUS_NOT_FOUND);
}
$file = $nodes[0];
if (!$file instanceof File) {
return new DataResponse(['error' => 'Not a file'], Http::STATUS_NOT_FOUND);
}
if ($this->previewManager->isAvailable($file)) {
$preview = $this->previewManager->getPreview($file, $size, $size);
$resp = new FileDisplayResponse($preview, Http::STATUS_OK, [
'Content-Type' => $preview->getMimeType(),
]);
} else {
$resp = new FileDisplayResponse($file, Http::STATUS_OK, [
'Content-Type' => $file->getMimeType(),
]);
}
$resp->cacheFor(3600);
return $resp;
}
private function requireUid(): string {
$user = $this->userSession->getUser();
if ($user === null) {
throw new ForbiddenException('Not authenticated');
}
return $user->getUID();
}
}

View File

@@ -32,6 +32,8 @@ use OCP\AppFramework\Db\Entity;
* @method void setNextDueAt(?int $nextDueAt)
* @method int|null getImageFileId()
* @method void setImageFileId(?int $imageFileId)
* @method string|null getImageUploadedBy()
* @method void setImageUploadedBy(?string $imageUploadedBy)
* @method int getSortOrder()
* @method void setSortOrder(int $sortOrder)
* @method int getCreatedAt()
@@ -51,6 +53,7 @@ class ChecklistItem extends Entity implements \JsonSerializable {
protected bool $repeatFromCompletion = false;
protected ?int $nextDueAt = null;
protected ?int $imageFileId = null;
protected ?string $imageUploadedBy = null;
protected int $sortOrder = 0;
protected int $createdAt = 0;
protected int $updatedAt = 0;
@@ -88,6 +91,7 @@ class ChecklistItem extends Entity implements \JsonSerializable {
'repeatFromCompletion' => $this->repeatFromCompletion,
'nextDueAt' => $this->nextDueAt,
'imageFileId' => $this->imageFileId,
'imageUploadedBy' => $this->imageUploadedBy,
'sortOrder' => $this->sortOrder,
'createdAt' => $this->createdAt,
'updatedAt' => $this->updatedAt,

View File

@@ -220,6 +220,10 @@ class Version1Date20260405000000 extends SimpleMigrationStep {
'notnull' => false,
'length' => 20,
]);
$table->addColumn('image_uploaded_by', Types::STRING, [
'notnull' => false,
'length' => 64,
]);
$table->addColumn('sort_order', Types::INTEGER, [
'notnull' => true,
'default' => 0,

View File

@@ -51,6 +51,7 @@ namespace OCA\Pantry;
* repeatFromCompletion: bool,
* nextDueAt: int|null,
* imageFileId: int|null,
* imageUploadedBy: string|null,
* sortOrder: int,
* createdAt: int,
* updatedAt: int,

View File

@@ -193,6 +193,10 @@ class ChecklistService {
if (array_key_exists('imageFileId', $patch)) {
$item->setImageFileId($this->intOrNull($patch['imageFileId']));
}
if (array_key_exists('imageUploadedBy', $patch)) {
$v = $patch['imageUploadedBy'];
$item->setImageUploadedBy(is_string($v) && $v !== '' ? $v : null);
}
// If already done and rrule or mode changed, recompute next due.
if ($item->getDone() && $item->getRrule() !== null
&& (array_key_exists('rrule', $patch) || array_key_exists('repeatFromCompletion', $patch))) {