big: add multipreview

cap
Varun Patil 2022-12-07 15:31:59 -08:00
parent aecc528f38
commit 0f31f845fb
4 changed files with 224 additions and 0 deletions

View File

@ -56,6 +56,7 @@ return [
['name' => 'Archive#archive', 'url' => '/api/archive/{id}', 'verb' => 'PATCH'],
['name' => 'Image#preview', 'url' => '/api/image/preview/{id}', 'verb' => 'GET'],
['name' => 'Image#multipreview', 'url' => '/api/image/multipreview', 'verb' => 'POST'],
['name' => 'Image#info', 'url' => '/api/image/info/{id}', 'verb' => 'GET'],
['name' => 'Image#setExif', 'url' => '/api/image/set-exif/{id}', 'verb' => 'PATCH'],
['name' => 'Image#jpeg', 'url' => '/api/image/jpeg/{id}', 'verb' => 'GET'],

View File

@ -29,6 +29,7 @@ use OCA\Memories\Exif;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\FileDisplayResponse;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Files\IRootFolder;
class ImageController extends ApiBase
{
@ -76,6 +77,90 @@ class ImageController extends ApiBase
}
}
/**
* @NoAdminRequired
*
* @NoCSRFRequired
*
* @PublicPage
*
* Get preview of many images
*/
public function multipreview() {
// read body to array
try {
$body = file_get_contents('php://input');
$files = json_decode($body, true);
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_BAD_REQUEST);
}
/** @var \OCP\IPreview $previewManager */
$previewManager = \OC::$server->get(\OCP\IPreview::class);
// For checking max previews
$previewRoot = new \OC\Preview\Storage\Root(
\OC::$server->get(IRootFolder::class),
\OC::$server->getSystemConfig(),
);
// stream the response
header('Content-Type: application/octet-stream');
header('Expires: ' . \gmdate('D, d M Y H:i:s \G\M\T', \time() + 7 * 3600 * 24));
header('Cache-Control: max-age=' . 7 * 3600 * 24 . ', private');
foreach ($files as $bodyFile) {
$reqid = $bodyFile['reqid'];
$fileid = (int) $bodyFile['fileid'];
$x = (int) $bodyFile['x'];
$y = (int) $bodyFile['y'];
$a = $bodyFile['a'] === '1';
$file = $this->getUserFile($fileid);
if (!$file) {
continue;
}
// Make sure max preview exists
$fileId = (string) $file->getId();
$folder = $previewRoot->getFolder($fileId);
$hasMax = false;
foreach ($folder->getDirectoryListing() as $preview) {
$name = $preview->getName();
if (str_contains($name, '-max')) {
$hasMax = true;
break;
}
}
if (!$hasMax) {
continue;
}
// Add this preview to the response
try {
$preview = $previewManager->getPreview($file, $x, $y, !$a, 'fill');
$content = $preview->getContent();
if (empty($content)) {
continue;
}
echo json_encode([
'reqid' => $reqid,
'Content-Length' => \strlen($content),
'Content-Type' => $preview->getMimeType(),
]);
echo "\n";
echo $content;
flush();
} catch (\Exception $e) {
continue;
}
}
exit;
}
/**
* @NoAdminRequired
*

View File

@ -0,0 +1,136 @@
import { registerRoute } from "workbox-routing";
interface FetchPreviewObject {
url: URL;
fileid: number;
reqid: number;
callback: (blob: Response) => void;
}
let fetchPreviewQueue: FetchPreviewObject[] = [];
let imageCache: Cache;
(async () => {
imageCache = await caches.open("images");
})();
let fetchPreviewTimer: any;
async function flushPreviewQueue() {
if (fetchPreviewQueue.length === 0) return;
fetchPreviewTimer = 0;
const fetchPreviewQueueCopy = fetchPreviewQueue;
fetchPreviewQueue = [];
const files = fetchPreviewQueueCopy.map((p) => ({
fileid: p.fileid,
x: Number(p.url.searchParams.get("x")),
y: Number(p.url.searchParams.get("y")),
a: p.url.searchParams.get("a"),
reqid: p.reqid,
}));
try {
// infer the url from the first file
const firstUrl = fetchPreviewQueueCopy[0].url;
const url = new URL(firstUrl.toString());
const path = url.pathname.split("/");
const previewIndex = path.indexOf("preview");
url.pathname = path.slice(0, previewIndex).join("/") + "/multipreview";
url.searchParams.delete("x");
url.searchParams.delete("y");
url.searchParams.delete("a");
url.searchParams.delete("c");
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(files),
});
if (res.status !== 200) throw new Error("Error fetching multi-preview");
const blob = await res.blob();
let idx = 0;
while (idx < blob.size) {
// Read a line of JSON from blob
const line = await blob.slice(idx, idx + 256).text();
const newlineIndex = line?.indexOf("\n");
const jsonParsed = JSON.parse(line?.slice(0, newlineIndex));
const imgLen = jsonParsed["Content-Length"];
const imgType = jsonParsed["Content-Type"];
const reqid = jsonParsed["reqid"];
idx += newlineIndex + 1;
console.debug("multi-preview", jsonParsed);
// Read the image data
const imgBlob = blob.slice(idx, idx + imgLen);
idx += imgLen;
// Initiate callbacks
fetchPreviewQueueCopy
.filter((p) => p.reqid === reqid)
.forEach((p) => {
p.callback(
new Response(imgBlob, {
status: 200,
headers: {
"Content-Type": imgType,
"Content-Length": imgLen,
Expires: res.headers.get("Expires"),
"Cache-Control": res.headers.get("Cache-Control"),
},
})
);
p.callback = null;
});
}
} catch (e) {
console.error(e);
}
// Initiate callbacks for failed requests
fetchPreviewQueueCopy.forEach((fetchPreviewObject) => {
fetchPreviewObject.callback?.(
new Response("Image not found", {
status: 404,
statusText: "Image not found",
})
);
});
}
registerRoute(
/^.*\/apps\/memories\/api\/image\/preview\/.*/,
async ({ url }) => {
// Check if in cache
const cache = await imageCache?.match(url);
if (cache) return cache;
// Get file id from URL
const fileid = Number(url.pathname.split("/").pop());
// Aggregate requests
let res: Response = await new Promise((callback) => {
fetchPreviewQueue.push({
url,
fileid,
reqid: Math.random(),
callback,
});
if (!fetchPreviewTimer) {
fetchPreviewTimer = setTimeout(flushPreviewQueue, 50);
}
});
if (res.status !== 200) {
res = await fetch(url);
}
// Cache response
if (res.status === 200) {
imageCache?.put(url, res.clone());
}
return res;
}
);

View File

@ -5,6 +5,8 @@ import { ExpirationPlugin } from 'workbox-expiration';
precacheAndRoute(self.__WB_MANIFEST);
import './service-worker-custom';
registerRoute(/^.*\/apps\/memories\/api\/video\/transcode\/.*/, new NetworkOnly());
registerRoute(/^.*\/apps\/memories\/api\/image\/jpeg\/.*/, new NetworkOnly());
registerRoute(/^.*\/remote.php\/.*/, new NetworkOnly());