big: remove filename from IPhoto

Signed-off-by: Varun Patil <varunpatil@ucla.edu>
pull/504/head
Varun Patil 2023-03-16 09:58:43 -07:00
parent 1851c463c5
commit b1df9215f9
21 changed files with 113 additions and 249 deletions

View File

@ -209,6 +209,11 @@ class ImageController extends ApiBase
// Inject permissions and convert to string
$info['permissions'] = \OCA\Memories\Util::permissionsToStr($file->getPermissions());
// Inject other file parameters that are cheap to get now
$info['mimetype'] = $file->getMimeType();
$info['size'] = $file->getSize();
$info['basename'] = $file->getName();
return new JSONResponse($info, Http::STATUS_OK);
}

View File

@ -23,7 +23,7 @@ class TimelineQuery
public const TIMELINE_SELECT = [
'm.isvideo', 'm.video_duration', 'm.datetaken', 'm.dayid', 'm.w', 'm.h', 'm.liveid',
'f.etag', 'f.path', 'f.name AS basename', 'mimetypes.mimetype',
'f.etag', 'f.name AS basename', 'mimetypes.mimetype',
];
protected IDBConnection $connection;

View File

@ -4,23 +4,20 @@ declare(strict_types=1);
namespace OCA\Memories\Db;
use OCA\Memories\Exif;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
const CTE_FOLDERS = // CTE to get all folders recursively in the given top folders excluding archive
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, rootid) AS (
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid) AS (
SELECT
f.fileid,
f.fileid AS rootid
f.fileid
FROM
*PREFIX*filecache f
WHERE
f.fileid IN (:topFolderIds)
UNION ALL
SELECT
f.fileid,
c.rootid
f.fileid
FROM
*PREFIX*filecache f
INNER JOIN *PREFIX*cte_folders_all c
@ -30,8 +27,7 @@ const CTE_FOLDERS = // CTE to get all folders recursively in the given top folde
)
), *PREFIX*cte_folders AS (
SELECT
fileid,
MIN(rootid) AS rootid
fileid
FROM
*PREFIX*cte_folders_all
GROUP BY
@ -39,11 +35,10 @@ const CTE_FOLDERS = // CTE to get all folders recursively in the given top folde
)';
const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the given top folders
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, name, rootid) AS (
'WITH RECURSIVE *PREFIX*cte_folders_all(fileid, name) AS (
SELECT
f.fileid,
f.name,
f.fileid AS rootid
f.name
FROM
*PREFIX*filecache f
WHERE
@ -51,18 +46,16 @@ const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the
UNION ALL
SELECT
f.fileid,
f.name,
c.rootid
f.name
FROM
*PREFIX*filecache f
INNER JOIN *PREFIX*cte_folders_all c
ON (f.parent = c.fileid
AND f.mimetype = (SELECT `id` FROM `*PREFIX*mimetypes` WHERE `mimetype` = \'httpd/unix-directory\')
)
), *PREFIX*cte_folders(fileid, rootid) AS (
), *PREFIX*cte_folders(fileid) AS (
SELECT
cfa.fileid,
MIN(cfa.rootid) AS rootid
cfa.fileid
FROM
*PREFIX*cte_folders_all cfa
WHERE
@ -71,8 +64,7 @@ const CTE_FOLDERS_ARCHIVE = // CTE to get all archive folders recursively in the
cfa.fileid
UNION ALL
SELECT
f.fileid,
c.rootid
f.fileid
FROM
*PREFIX*filecache f
INNER JOIN *PREFIX*cte_folders c
@ -160,11 +152,6 @@ trait TimelineQueryDays
// JOIN with filecache for existing files
$query = $this->joinFilecache($query, $root, $recursive, $archive);
// SELECT rootid if not a single folder
if ($recursive && !$root->isEmpty()) {
$query->addSelect('cte_f.rootid');
}
// JOIN with mimetypes to get the mimetype
$query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id'));
@ -213,56 +200,6 @@ trait TimelineQueryDays
*/
private function processDay(array &$day, string $uid, TimelineRoot &$root)
{
/**
* Path entry in database for folder.
* We need to splice this from the start of the file path.
*/
$internalPaths = [];
/**
* DAV paths for the folders.
* We need to prefix this to the start of the file path.
*/
$davPaths = [];
/**
* The root folder id for the folder.
* We fallback to this if rootid is not found.
*/
$defaultRootId = 0;
if (!$root->isEmpty()) {
// Get root id of the top folder
$defaultRootId = $root->getOneId();
// No way to get the internal path from the folder
$query = $this->connection->getQueryBuilder();
$query->select('fileid', 'path')
->from('filecache')
->where($query->expr()->in('fileid', $query->createNamedParameter($root->getIds(), IQueryBuilder::PARAM_INT_ARRAY)))
;
$paths = $query->executeQuery()->fetchAll();
foreach ($paths as &$path) {
$fileid = (int) $path['fileid'];
$internalPaths[$fileid] = $path['path'];
// Get DAV path.
// getPath looks like /user/files/... but we want /files/user/...
// Split at / and swap these
// For public shares, we just give the relative path
if (!empty($uid) && ($actualPath = $root->getFolderPath($fileid))) {
$actualPath = explode('/', $actualPath);
if (\count($actualPath) >= 3) {
$tmp = $actualPath[1];
$actualPath[1] = $actualPath[2];
$actualPath[2] = $tmp;
$davPath = implode('/', $actualPath);
$davPaths[$fileid] = Exif::removeExtraSlash('/'.$davPath.'/');
}
}
}
}
foreach ($day as &$row) {
// Convert field types
$row['fileid'] = (int) $row['fileid'];
@ -282,26 +219,12 @@ trait TimelineQueryDays
unset($row['liveid']);
}
// Check if path exists and starts with basePath and remove
if (isset($row['path']) && !empty($row['path'])) {
$rootId = \array_key_exists('rootid', $row) ? $row['rootid'] : $defaultRootId;
$basePath = $internalPaths[$rootId] ?? '#__#';
$davPath = (\array_key_exists($rootId, $davPaths) ? $davPaths[$rootId] : null) ?: '';
if (0 === strpos($row['path'], $basePath)) {
$rpath = substr($row['path'], \strlen($basePath));
$row['filename'] = Exif::removeExtraSlash($davPath.$rpath);
}
unset($row['path']);
}
// All transform processing
$this->processPeopleRecognizeDetection($row);
$this->processFaceRecognitionDetection($row);
// We don't need these fields
unset($row['datetaken'], $row['rootid']);
unset($row['datetaken']);
}
return $day;

View File

@ -25,8 +25,6 @@ trait TimelineQuerySingleItem
// JOIN with mimetypes to get the mimetype
$query->join('f', 'mimetypes', 'mimetypes', $query->expr()->eq('f.mimetype', 'mimetypes.id'));
unset($row['datetaken'], $row['path']);
return $query->executeQuery()->fetch();
}
}

View File

@ -200,11 +200,6 @@ export default defineComponent({
// Register sidebar metadata tab
const OCA = globalThis.OCA;
if (OCA.Files && OCA.Files.Sidebar) {
const pf = (fileInfo) => {
fileInfo.fileid = Number(fileInfo.id);
return fileInfo;
};
OCA.Files.Sidebar.registerTab(
new OCA.Files.Sidebar.Tab({
id: "memories-metadata",
@ -215,10 +210,10 @@ export default defineComponent({
this.metadataComponent?.$destroy?.();
this.metadataComponent = new Vue(Metadata as any);
this.metadataComponent.$mount(el);
this.metadataComponent.update(pf(fileInfo));
this.metadataComponent.update(Number(fileInfo.id));
},
update(fileInfo) {
this.metadataComponent.update(pf(fileInfo));
this.metadataComponent.update(Number(fileInfo.id));
},
destroy() {
this.metadataComponent?.$destroy?.();

View File

@ -1,5 +1,5 @@
<template>
<div class="outer" v-if="fileInfo">
<div class="outer" v-if="fileid">
<div class="top-field" v-for="field of topFields" :key="field.title">
<div class="icon">
<component :is="field.icon" :size="24" />
@ -62,8 +62,6 @@ import moment from "moment";
import * as utils from "../services/Utils";
import { IFileInfo } from "../types";
import EditIcon from "vue-material-design-icons/Pencil.vue";
import CalendarIcon from "vue-material-design-icons/Calendar.vue";
import CameraIrisIcon from "vue-material-design-icons/CameraIris.vue";
@ -72,6 +70,7 @@ import InfoIcon from "vue-material-design-icons/InformationOutline.vue";
import LocationIcon from "vue-material-design-icons/MapMarker.vue";
import TagIcon from "vue-material-design-icons/Tag.vue";
import { API } from "../services/API";
import { IImageInfo } from "../types";
interface TopField {
title: string;
@ -91,9 +90,9 @@ export default defineComponent({
},
data: () => ({
fileInfo: null as IFileInfo,
fileid: null as number | null,
exif: {} as { [prop: string]: any },
baseInfo: {} as any,
baseInfo: {} as IImageInfo,
state: 0,
}),
@ -246,11 +245,7 @@ export default defineComponent({
/** Image info */
imageInfo(): string | null {
return (
this.fileInfo?.originalBasename ||
this.fileInfo?.basename ||
(<any>this.fileInfo)?.name
);
return this.baseInfo.basename;
},
imageInfoSub(): string[] | null {
@ -310,24 +305,25 @@ export default defineComponent({
},
methods: {
async update(fileInfo: IFileInfo) {
async update(fileid: number): Promise<IImageInfo> {
this.state = Math.random();
this.fileInfo = null;
this.fileid = null;
this.exif = {};
const state = this.state;
const url = API.Q(API.IMAGE_INFO(fileInfo.fileid), { tags: 1 });
const url = API.Q(API.IMAGE_INFO(fileid), { tags: 1 });
const res = await axios.get<any>(url);
if (state !== this.state) return;
if (state !== this.state) return res.data;
this.fileInfo = fileInfo;
this.fileid = fileid;
this.exif = res.data.exif || {};
this.baseInfo = res.data;
return this.baseInfo;
},
handleFileUpdated({ fileid }) {
if (fileid && this.fileInfo?.id === fileid) {
this.update(this.fileInfo);
if (fileid && this.fileid === fileid) {
this.update(this.fileid);
}
},
},

View File

@ -23,7 +23,7 @@ import NcActions from "@nextcloud/vue/dist/Components/NcActions";
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
import Metadata from "./Metadata.vue";
import { IFileInfo } from "../types";
import { IImageInfo } from "../types";
import CloseIcon from "vue-material-design-icons/Close.vue";
@ -61,20 +61,17 @@ export default defineComponent({
},
methods: {
async open(file: IFileInfo) {
if (
!this.reducedOpen &&
this.native() &&
(!file.fileid || file.originalFilename?.startsWith("/files/"))
) {
async open(fileid: number, filename?: string, forceNative = false) {
if (!this.reducedOpen && this.native() && (!fileid || forceNative)) {
this.native()?.setFullScreenMode?.(true);
this.native()?.open(file.filename);
this.native()?.open(filename);
} else {
this.reducedOpen = true;
await this.$nextTick();
this.basename = file.originalBasename || file.basename;
(<any>this.$refs.metadata)?.update(file);
const info: IImageInfo = await (<any>this.$refs.metadata)?.update(
fileid
);
this.basename = info.basename;
emit("memories:sidebar:opened", null);
}
},

View File

@ -996,9 +996,7 @@ export default defineComponent({
head.day.detail.length === photos.length &&
head.day.detail.every(
(p, i) =>
p.fileid === photos[i].fileid &&
p.etag === photos[i].etag &&
p.filename === photos[i].filename
p.fileid === photos[i].fileid && p.etag === photos[i].etag
)
) {
continue;

View File

@ -193,7 +193,7 @@ img {
font-size: 1em;
word-wrap: break-word;
text-overflow: ellipsis;
line-height: 1.2em;
line-height: 1.1em;
> .subtitle {
font-size: 0.7em;

View File

@ -114,10 +114,8 @@ export default defineComponent({
photoMap.set(photo.fileid, photo);
}
let data = await dav.getFiles(this.photos);
// Create move calls
const calls = data.map((p) => async () => {
const calls = this.photos.map((p) => async () => {
try {
await client.moveFile(
`/recognize/${user}/faces/${name}/${p.fileid}-${p.basename}`,

View File

@ -68,7 +68,7 @@ export default defineComponent({
mounted() {
if (this.sidebar) {
globalThis.mSidebar.open({ filename: this.sidebar } as any);
globalThis.mSidebar.open(0, this.sidebar, true);
// Adjust width anyway in case the sidebar is already open
this.handleAppSidebarOpen();

View File

@ -250,7 +250,7 @@ export default defineComponent({
refreshSidebar() {
if (this.isMobile) return;
globalThis.mSidebar.close();
globalThis.mSidebar.open({ filename: this.filename } as any);
globalThis.mSidebar.open(0, this.filename, true);
},
},
});

View File

@ -83,7 +83,7 @@ import NcListItem from "@nextcloud/vue/dist/Components/NcListItem";
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon";
import Modal from "./Modal.vue";
import { IFileInfo, IPhoto } from "../../types";
import { IPhoto } from "../../types";
import { getPreviewUrl } from "../../services/FileUtils";
import { API } from "../../services/API";
import * as dav from "../../services/DavRequests";
@ -159,14 +159,6 @@ export default defineComponent({
}
},
async getFileInfo() {
if (this.$route.name.endsWith("-share")) {
return this.photo as IFileInfo;
}
return (await dav.getFiles([this.photo]))[0];
},
async sharePreview() {
const src = getPreviewUrl(this.photo, false, 2048);
this.shareWithHref(src, true);
@ -185,26 +177,26 @@ export default defineComponent({
},
async shareLink() {
this.l(async () =>
globalThis.shareNodeLink((await this.getFileInfo()).filename, true)
);
this.l(async () => {
const fileInfo = (await dav.getFiles([this.photo]))[0];
globalThis.shareNodeLink(fileInfo.filename, true);
});
this.close();
},
async shareWithHref(href: string, replaceExt = false) {
let fileInfo: IFileInfo, blob: Blob;
let blob: Blob;
await this.l(async () => {
const res = await axios.get(href, { responseType: "blob" });
blob = res.data;
fileInfo = await this.getFileInfo();
});
if (!blob || !fileInfo) {
showError(this.t("memories", "Failed to download and share file"));
if (!blob) {
showError(this.t("memories", "Failed to download file"));
return;
}
let basename = fileInfo.originalBasename || fileInfo.basename;
let basename = this.photo.basename;
if (replaceExt) {
// Fix basename extension

View File

@ -10,7 +10,7 @@
<ImageEditor
v-if="editorOpen"
:etag="currentPhoto.etag"
:src="editorDownloadLink"
:src="editorSrc"
:fileid="currentPhoto.fileid"
@close="editorOpen = false"
/>
@ -178,7 +178,7 @@
<script lang="ts">
import { defineComponent } from "vue";
import { IDay, IFileInfo, IPhoto, IRow, IRowType } from "../../types";
import { IDay, IPhoto, IRow, IRowType } from "../../types";
import UserConfig from "../../mixins/UserConfig";
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
@ -241,6 +241,7 @@ export default defineComponent({
isOpen: false,
originalTitle: null,
editorOpen: false,
editorSrc: "",
show: false,
showControls: false,
@ -351,14 +352,6 @@ export default defineComponent({
return utils.getLongDateStr(new Date(date * 1000), false, true);
},
/** Get DAV download link for current photo */
editorDownloadLink(): string | null {
const filename = this.currentPhoto?.filename;
return filename
? window.location.origin + getRootUrl() + `/remote.php/dav${filename}`
: null;
},
/** Show edit buttons */
canEdit(): boolean {
return this.currentPhoto?.imageInfo?.permissions?.includes("U");
@ -897,7 +890,7 @@ export default defineComponent({
}
},
openEditor() {
async openEditor() {
// Only for JPEG for now
if (!this.canEdit) return;
@ -909,6 +902,18 @@ export default defineComponent({
return;
}
// Get DAV path
const fileInfo = (await dav.getFiles([this.currentPhoto]))[0];
if (!fileInfo) {
alert(this.t("memories", "Cannot edit this file"));
return;
}
this.editorSrc =
window.location.origin +
getRootUrl() +
"/remote.php/dav" +
fileInfo.originalFilename;
this.editorOpen = true;
},
@ -1019,14 +1024,15 @@ export default defineComponent({
/** Open the sidebar */
async openSidebar(photo?: IPhoto) {
globalThis.mSidebar.setTab("memories-metadata");
globalThis.mSidebar.open(await this.getFileInfo(photo));
},
photo ||= this.currentPhoto;
/** Get fileInfo for a photo */
async getFileInfo(photo?: IPhoto): Promise<IFileInfo> {
photo = photo || this.currentPhoto;
if (this.routeIsPublic) return photo as IFileInfo;
return (await dav.getFiles([photo]))[0];
if (this.routeIsPublic) {
globalThis.mSidebar.open(photo.fileid);
} else {
const fileInfo = (await dav.getFiles([photo]))[0];
const forceNative = fileInfo?.originalFilename?.startsWith("/files/");
globalThis.mSidebar.open(photo.fileid, fileInfo?.filename, forceNative);
}
},
async updateSizeWithoutAnim() {

View File

@ -12,7 +12,7 @@ import router from "./router";
import { Route } from "vue-router";
import { generateFilePath } from "@nextcloud/router";
import { getRequestToken } from "@nextcloud/auth";
import { IFileInfo, IPhoto } from "./types";
import { IPhoto } from "./types";
import "./global.scss";
@ -27,7 +27,7 @@ declare global {
var shareNodeLink: (path: string, immediate?: boolean) => Promise<void>;
var mSidebar: {
open: (filename: IFileInfo) => void;
open: (fileid: number, filename?: string, forceNative?: boolean) => void;
close: () => void;
setTab: (tab: string) => void;
};

View File

@ -20,7 +20,7 @@
*
*/
import camelcase from "camelcase";
import { IFileInfo, IPhoto } from "../types";
import { IPhoto } from "../types";
import { API } from "./API";
import { isNumber } from "./NumberUtils";
@ -136,7 +136,7 @@ const genFileInfo = function (obj) {
/** Get preview URL from photo object */
const getPreviewUrl = function (
photo: IPhoto | IFileInfo,
photo: IPhoto,
square: boolean,
size: number | [number, number]
) {

View File

@ -118,11 +118,8 @@ export async function* removeFromAlbum(
name: string,
photos: IPhoto[]
) {
// Get files data
let fileInfos = await base.getFiles(photos);
// Add each file
const calls = fileInfos.map((f) => async () => {
const calls = photos.map((f) => async () => {
try {
await client.deleteFile(
`/photos/${user}/albums/${name}/${f.fileid}-${f.basename}`
@ -131,7 +128,7 @@ export async function* removeFromAlbum(
} catch (e) {
showError(
t("memories", "Failed to remove {filename}.", {
filename: f.filename,
filename: f.basename,
})
);
return 0;
@ -285,10 +282,6 @@ export function getAlbumFileInfos(
filename: `${collection}/${basename}`,
originalFilename: `${collection}/${basename}`,
basename: basename,
originalBasename: photo.basename,
mime: photo.mimetype,
hasPreview: true,
etag: photo.etag,
} as IFileInfo;
});
}

View File

@ -56,33 +56,8 @@ export async function getFiles(photos: IPhoto[]): Promise<IFileInfo[]> {
// Get file infos
let fileInfos: IFileInfo[] = [];
// Get all photos that already have and don't have a filename
const photosWithFilename = photos.filter((photo) => photo.filename);
fileInfos = fileInfos.concat(
photosWithFilename.map((photo) => {
const prefixPath = `/files/${getCurrentUser()?.uid}`;
return {
id: photo.fileid,
fileid: photo.fileid,
filename: photo.filename.replace(prefixPath, ""),
originalFilename: photo.filename,
basename: photo.basename,
mime: photo.mimetype,
hasPreview: true,
etag: photo.etag,
permissions: "RWD",
} as IFileInfo;
})
);
// Next: get all photos that have no filename using ID
if (photosWithFilename.length === photos.length) {
return fileInfos;
}
const photosWithoutFilename = photos.filter((photo) => !photo.filename);
// Get file IDs array
const fileIds = photosWithoutFilename.map((photo) => photo.fileid);
const fileIds = photos.map((photo) => photo.fileid);
// Divide fileIds into chunks of GET_FILE_CHUNK_SIZE
const chunks = [];

View File

@ -93,11 +93,8 @@ export async function* removeFaceImages(
name: string,
photos: IPhoto[]
) {
// Get files data
let fileInfos = await base.getFiles(photos);
// Remove each file
const calls = fileInfos.map((f) => async () => {
const calls = photos.map((f) => async () => {
try {
await client.deleteFile(
`/recognize/${user}/faces/${name}/${f.fileid}-${f.basename}`
@ -107,7 +104,7 @@ export async function* removeFaceImages(
console.error(e);
showError(
t("memories", "Failed to remove {filename} from face.", {
filename: f.filename,
filename: f.basename,
})
);
return 0;

View File

@ -1,6 +1,6 @@
import { showError } from "@nextcloud/dialogs";
import { translate as t } from "@nextcloud/l10n";
import { IPhoto } from "../../types";
import { IFileInfo, IPhoto } from "../../types";
import client from "../DavClient";
import * as base from "./base";
import * as utils from "../Utils";
@ -45,7 +45,7 @@ export async function* favoritePhotos(
}
// Get files data
let fileInfos: any[] = [];
let fileInfos: IFileInfo[] = [];
try {
fileInfos = await base.getFiles(photos);
} catch (e) {

View File

@ -9,20 +9,6 @@ export type IFileInfo = {
originalFilename?: string;
/** Base name of file e.g. Qx0dq7dvEXA.jpg */
basename: string;
/** Original base name, e.g. in albums without the file id */
originalBasename?: string;
/** Etag identifier */
etag: string;
/** File has preview available */
hasPreview: boolean;
/** File is marked favorite */
favorite?: boolean;
/** Vue flags */
flag?: number;
/** MIME type of file */
mime?: string;
/** WebDAV permissions string */
permissions?: string;
};
export type IDay = {
@ -43,8 +29,6 @@ export type IPhoto = {
key?: string;
/** Etag from server */
etag?: string;
/** Path to file */
filename?: string;
/** Base name of file */
basename?: string;
/** Mime type of file */
@ -74,23 +58,7 @@ export type IPhoto = {
/** Reference to day object */
d?: IDay;
/** Reference to exif object */
imageInfo?: {
h: number;
w: number;
datetaken: number;
address?: string;
tags: { [id: string]: string };
permissions: string;
exif?: {
Rotation?: number;
Orientation?: number;
ImageWidth?: number;
ImageHeight?: number;
Title?: string;
Description?: string;
[other: string]: unknown;
};
};
imageInfo?: IImageInfo;
/** Face detection ID */
faceid?: number;
@ -117,6 +85,29 @@ export type IPhoto = {
datetaken?: number;
};
export interface IImageInfo {
h: number;
w: number;
datetaken: number;
address?: string;
tags: { [id: string]: string };
permissions: string;
basename: string;
mimetype: string;
size: number;
exif?: {
Rotation?: number;
Orientation?: number;
ImageWidth?: number;
ImageHeight?: number;
Title?: string;
Description?: string;
[other: string]: unknown;
};
}
export interface IFolder extends IPhoto {
/** Path to folder */
path: string;