diff --git a/appinfo/routes.php b/appinfo/routes.php index b54e2e78a..6246b6afe 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -56,6 +56,8 @@ function w($base, $param) ['name' => 'Map#clusters', 'url' => '/api/map/clusters', 'verb' => 'GET'], ['name' => 'Map#init', 'url' => '/api/map/init', 'verb' => 'GET'], + ['name' => 'Uid#name', 'url' => '/api/uid/name/{id}', 'verb' => 'GET'], + ['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'], ['name' => 'Image#preview', 'url' => '/api/image/preview/{id}', 'verb' => 'GET'], diff --git a/lib/Controller/UidController.php b/lib/Controller/UidController.php new file mode 100644 index 000000000..991a26251 --- /dev/null +++ b/lib/Controller/UidController.php @@ -0,0 +1,55 @@ + + * @author Varun Patil + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\Memories\Controller; + +use OCA\Memories\Util; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; + +class UidController extends GenericApiController +{ + /** + * @NoAdminRequired + * + * @NoCSRFRequired + * + * @PublicPage + * + * Get display name for a Nextcloud user id + * + * @param string fileid + */ + public function name( + string $uid, + ): Http\Response { + return Util::guardEx(static function () use ($uid) { + $userManager = \OC::$server->get(\OCP\IUserManager::class); + $user = $userManager->get($uid); + + return new JSONResponse([ + 'user_display' => $user ? $user->getDisplayName() : null, + ], Http::STATUS_OK); + }); + } +} diff --git a/lib/Db/TimelineQuery.php b/lib/Db/TimelineQuery.php index c3e719679..944fb12b4 100644 --- a/lib/Db/TimelineQuery.php +++ b/lib/Db/TimelineQuery.php @@ -25,6 +25,7 @@ class TimelineQuery 'f.etag', 'f.name AS basename', 'f.size', 'm.epoch', // auid 'mimetypes.mimetype', + 'm.uid', ]; protected ?TimelineRoot $_root = null; // cache diff --git a/lib/Db/TimelineQueryDays.php b/lib/Db/TimelineQueryDays.php index 285558a38..a1736cad5 100644 --- a/lib/Db/TimelineQueryDays.php +++ b/lib/Db/TimelineQueryDays.php @@ -7,6 +7,7 @@ use OCA\Memories\ClustersBackend; use OCA\Memories\Exif; use OCA\Memories\Settings\SystemConfig; +use OCA\Memories\Util; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -315,6 +316,10 @@ private function postProcessDayPhoto(array &$row, bool $monthView = false): void unset($row['liveid']); } + if ($row['uid'] === Util::getUID()) { + unset($row['uid']); + } + // Favorite field, may not be present if ($row['categoryid'] ?? null) { $row['isfavorite'] = 1; diff --git a/lib/Db/TimelineQuerySingleItem.php b/lib/Db/TimelineQuerySingleItem.php index d690c4d42..163161943 100644 --- a/lib/Db/TimelineQuerySingleItem.php +++ b/lib/Db/TimelineQuerySingleItem.php @@ -45,7 +45,7 @@ public function getSingleItem(int $fileId): ?array public function getInfoById(int $id, bool $basic): array { $qb = $this->connection->getQueryBuilder(); - $qb->select('fileid', 'dayid', 'datetaken', 'w', 'h') + $qb->select('fileid', 'dayid', 'datetaken', 'w', 'h', 'uid') ->from('memories') ->where($qb->expr()->eq('fileid', $qb->createNamedParameter($id, \PDO::PARAM_INT))) ; @@ -63,6 +63,7 @@ public function getInfoById(int $id, bool $basic): array 'w' => (int) $row['w'], 'h' => (int) $row['h'], 'datetaken' => Util::sqlUtcToTimestamp($row['datetaken']), + 'uid' => $row['uid'], ]; // Return if only basic info is needed diff --git a/lib/Db/TimelineWrite.php b/lib/Db/TimelineWrite.php index 0e38d50fa..a942e0a8b 100644 --- a/lib/Db/TimelineWrite.php +++ b/lib/Db/TimelineWrite.php @@ -72,6 +72,7 @@ public function processFile( // Get parameters $mtime = $file->getMtime(); $fileId = $file->getId(); + $uid = $file->getOwner()?->getUID(); $isvideo = Index::isVideo($file); // Get previous row @@ -172,6 +173,7 @@ public function processFile( 'orphan' => $query->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), 'buid' => $query->createNamedParameter($buid, IQueryBuilder::PARAM_STR), 'parent' => $query->createNamedParameter($file->getParent()->getId(), IQueryBuilder::PARAM_INT), + 'uid' => $query->createNamedParameter($uid, IQueryBuilder::PARAM_STR), ]; // There is no easy way to UPSERT in standard SQL diff --git a/lib/Migration/Version800001Date20241026171056.php b/lib/Migration/Version800001Date20241026171056.php new file mode 100644 index 000000000..0e63bbcc5 --- /dev/null +++ b/lib/Migration/Version800001Date20241026171056.php @@ -0,0 +1,120 @@ + + * @author Varun Patil + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\Memories\Migration; + +use OCP\DB\ISchemaWrapper; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * FIXME Auto-generated migration step: Please modify to your needs! + */ +class Version800001Date20241026171056 extends SimpleMigrationStep +{ + public function __construct(private IDBConnection $dbc) {} + + /** + * @param \Closure(): ISchemaWrapper $schemaClosure + */ + public function preSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void {} + + /** + * @param \Closure(): ISchemaWrapper $schemaClosure + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options): ?ISchemaWrapper + { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + if (!$schema->hasTable('memories')) { + throw new \Exception('Memories table does not exist'); + } + + $table = $schema->getTable('memories'); + + if (!$table->hasColumn('uid')) { + $table->addColumn('uid', 'string', [ + 'notnull' => false, + 'length' => 64, + ]); + } + + return $schema; + } + + /** + * @param \Closure(): ISchemaWrapper $schemaClosure + */ + public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options): void + { + // create database triggers; this will never throw + \OCA\Memories\Db\AddMissingIndices::createFilecacheTriggers($output); + + // migrate uid values from filecache + try { + $output->info('Migrating values for uid from filecache'); + + $platform = $this->dbc->getDatabasePlatform(); + + // copy existing parent values from filecache + if (preg_match('/mysql|mariadb/i', $platform::class)) { + $this->dbc->executeQuery( + 'UPDATE *PREFIX*memories AS m + JOIN *PREFIX*filecache AS f ON f.fileid = m.fileid + JOIN *PREFIX*storages AS s ON f.storage = s.numeric_id + SET m.uid = SUBSTRING_INDEX(s.id, \'::\', -1) + WHERE s.id LIKE \'home::%\' + ', + ); + } elseif (preg_match('/postgres/i', $platform::class)) { + $this->dbc->executeQuery( + 'UPDATE *PREFIX*memories AS m + SET uid = split_part(s.id, \'::\', 2) + FROM *PREFIX*filecache AS f + JOIN *PREFIX*storages AS s ON f.storage = s.numeric_id + WHERE f.fileid = m.fileid + AND s.id LIKE \'home::%\' + ', + ); + } elseif (preg_match('/sqlite/i', $platform::class)) { + $this->dbc->executeQuery( + 'UPDATE memories AS m + SET uid = SUBSTR(s.id, INSTR(s.id, \'::\') + 2) + FROM filecache AS f + JOIN storages AS s ON f.storage = s.numeric_id + WHERE f.fileid = m.fileid + AND s.id LIKE \'home::%\' + ', + ); + } else { + throw new \Exception('Unsupported '.$platform::class); + } + + $output->info('Values for uid migrated successfully'); + } catch (\Exception $e) { + $output->warning('Failed to copy uid values from fileid: '.$e->getMessage()); + $output->warning('Please run occ memories:index -f'); + } + } +} diff --git a/src/components/Metadata.vue b/src/components/Metadata.vue index 63be50354..6934788db 100644 --- a/src/components/Metadata.vue +++ b/src/components/Metadata.vue @@ -17,6 +17,19 @@ +
+
{{ t('memories', 'Shared By') }}
+
+
+ +
+ +
+ {{ userDisplay }} +
+
+
+
{{ t('memories', 'Metadata') }}
@@ -90,9 +103,10 @@ import TagIcon from 'vue-material-design-icons/Tag.vue'; import * as utils from '@services/utils'; import * as dav from '@services/dav'; import { API } from '@services/API'; - import type { IAlbum, IFace, IImageInfo, IPhoto, IExif } from '@typings'; +const NcAvatar = () => import('@nextcloud/vue/dist/Components/NcAvatar.js'); + interface TopField { id?: string; title: string; @@ -110,6 +124,7 @@ export default defineComponent({ AlbumsList, Cluster, EditIcon, + NcAvatar, }, mixins: [UserConfig], @@ -120,6 +135,8 @@ export default defineComponent({ exif: {} as IExif, baseInfo: {} as IImageInfo, error: false, + uid: null as string | null, + userDisplay: '', loading: 0, state: 0, @@ -419,6 +436,11 @@ export default defineComponent({ this.filename = this.baseInfo.basename; this.exif = this.baseInfo.exif ?? {}; + // set user info + this.uid = this.baseInfo.uid ?? null; + if (this.uid == utils.uid) this.uid = null; + this.userDisplay = await utils.getUserDisplayName(this.uid); + return this.baseInfo; }, diff --git a/src/components/frame/Photo.vue b/src/components/frame/Photo.vue index 23d9ef170..4b2968a2f 100644 --- a/src/components/frame/Photo.vue +++ b/src/components/frame/Photo.vue @@ -29,11 +29,16 @@
-
+
+
+ + {{ owner }} +
+
& { + transform: translate($icon-size, -$icon-size); + } + } + + > .username { + font-size: 0.75em; + font-weight: bold; + margin-left: 3px; + } + > .video { display: flex; line-height: 22px; // force text height to match diff --git a/src/services/API.ts b/src/services/API.ts index effb62a0f..35319188e 100644 --- a/src/services/API.ts +++ b/src/services/API.ts @@ -221,4 +221,8 @@ export class API { static MAP_INIT() { return tok(gen(`${BASE}/map/init`)); } + + static UID_NAME(uid: string) { + return tok(gen(`${BASE}/uid/name/{uid}`, { uid })); + } } diff --git a/src/services/utils/index.ts b/src/services/utils/index.ts index 22ea6cd2f..62869e744 100644 --- a/src/services/utils/index.ts +++ b/src/services/utils/index.ts @@ -6,3 +6,4 @@ export * from './helpers'; export * from './dialog'; export * from './event-bus'; export * from './fragment'; +export * from './user'; diff --git a/src/services/utils/user.ts b/src/services/utils/user.ts new file mode 100644 index 000000000..65fd7fbf1 --- /dev/null +++ b/src/services/utils/user.ts @@ -0,0 +1,19 @@ +import axios from '@nextcloud/axios'; +import { API } from '@services/API'; +import * as utils from '@services/utils'; + +export async function getUserDisplayName(uid: string | null): Promise { + if (!uid) return ''; + + // First look in cache + const cacheUrl = API.UID_NAME(uid); + const cache = await utils.getCachedData(cacheUrl); + if (cache) return cache; + + // Network request and update cache + const { data } = await axios.get(API.Q(cacheUrl, { uid })); + const name = data?.user_display ?? ''; + utils.cacheData(cacheUrl, name); + + return name; +} diff --git a/src/typings/data.d.ts b/src/typings/data.d.ts index d95dc2676..c2035cbc0 100644 --- a/src/typings/data.d.ts +++ b/src/typings/data.d.ts @@ -99,6 +99,8 @@ declare module '@typings' { /** Stacked RAW photos */ stackraw?: IPhoto[]; + + uid?: string; }; export interface IImageInfo { @@ -125,6 +127,8 @@ declare module '@typings' { recognize?: IFace[]; facerecognition?: IFace[]; }; + + uid?: string; } export interface IExif {