Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 53 additions & 19 deletions packages/core/hooks/useOpenWithMenuItems/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ type VolEMessage = {
function getSupportedApps(
apps: Apps,
isSmallFile: boolean,
fileContentType: "webpage" | "image" | "multi-object" | "unknown",
fileDetails?: FileDetail
): IContextualMenuItem[] {
if (!fileDetails) {
Expand Down Expand Up @@ -261,6 +262,26 @@ function getSupportedApps(
// Not a valid URL; skip IDR check
}

// If the content of the file linked appears to be a webpage
// then offer Browser as the only option
if (fileContentType === "webpage") {
let hostname: string | undefined;
try {
hostname = new URL(fileDetails.path).hostname;
} catch (_e) {
// Somehow, not a valid URL; skip formatting hostname
}
return [
{
...apps.browser,
text: hostname ? `Browser (${hostname})` : "Browser",
title: hostname
? `Open ${hostname} in the current browser in a new tab`
: "Open in the current browser in a new tab",
},
];
}

return isLikelyLocalFile
? [apps.agave, apps.neuroglancer, apps.vole, apps.cfe]
: [apps.vole, apps.neuroglancer, apps.agave, apps.cfe, apps.validator];
Expand Down Expand Up @@ -359,6 +380,9 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe
);
const [isSmallFile, setIsSmallFile] = React.useState(false);
const [isMacOS, setIsMacOS] = React.useState(false);
const [fileContentType, setFileContentType] = React.useState<
"webpage" | "image" | "multi-object" | "unknown"
>("unknown");

const openInCfe = useOpenInCfe(fileSelection, annotationNames, fileService);

Expand Down Expand Up @@ -494,25 +518,32 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe

// Determine is the file is small or not asynchronously
React.useEffect(() => {
async function determineFileSize() {
if (path) {
let fileSize = size;
if (!fileSize) {
try {
fileSize = await s3StorageService.getCloudObjectSize(path);
} catch (_err) {
console.debug(
`Failed to get size of ${path}. Unable to determine if Vol-E is suitable viewer.`
);
}
}

if (!path) return;

// Reset state between file changes
setFileContentType("unknown");
// Consider a "small" file to be <= 100Mb
setIsSmallFile(size === undefined ? false : size <= 100 * ONE_MEGABYTE);

// Grab object info.
// Trust the users size if it is defined, otherwise get the size from the cloud object info.
// Also get the content type of the file, which may be used to determine the most suitable viewer.
s3StorageService
.getCloudObjectInfo(path)
.then((info) => {
// Consider a "small" file to be <= 100Mb
setIsSmallFile(!!fileSize && fileSize <= 100 * ONE_MEGABYTE);
}
}
determineFileSize();
}, [path, size, s3StorageService, setIsSmallFile]);
if (size === undefined && info.size !== undefined)
setIsSmallFile(info.size <= 100 * ONE_MEGABYTE);
setFileContentType(info.type);
})
.catch((_err) => {
console.debug(
`Failed to get size or type of ${path}. Unable to determine most suitable viewer.`
);
});
// We only want this to re-run when the path changes, not when the size changes
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [path, s3StorageService]);

// Try to quickly check if user is on MacOS or not
React.useEffect(() => {
Expand All @@ -537,7 +568,10 @@ export default (fileDetails?: FileDetail, filters?: FileFilter[]): IContextualMe
getIsMacOS().then(setIsMacOS);
}, []);

const supportedApps = [...getSupportedApps(apps, isSmallFile, fileDetails), ...userApps]
const supportedApps = [
...getSupportedApps(apps, isSmallFile, fileContentType, fileDetails),
...userApps,
]
// TODO: This is a placeholder until FIJI finishes rolling out FIJI support across all
// platforms
.filter((app) => isMacOS || app.key !== AppKeys.FIJI);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ export default class DatabaseFileService implements FileService {
grouped.set(head, { isArray: headIsArray, subPaths: [] });
}
if (tailSegments.length > 0) {
grouped.get(head)!.subPaths.push({ segments: tailSegments, isArray: tailIsArray });
grouped.get(head)?.subPaths.push({ segments: tailSegments, isArray: tailIsArray });
}
}

Expand Down
55 changes: 40 additions & 15 deletions packages/core/services/S3StorageService/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { parseS3Url, isS3Url } from "amazon-s3-url";
import axios from "axios";
import axios, { AxiosResponse } from "axios";
import { isNil } from "lodash";

import HttpServiceBase, { ConnectionConfig } from "../HttpServiceBase";

interface HttpInfo {
type: "webpage" | "image" | "multi-object" | "unknown";
size?: number;
}

/**
* Return true if the URL seems to point to a multi object file like Zarr
*/
Expand Down Expand Up @@ -67,18 +73,29 @@ export default class S3StorageService extends HttpServiceBase {
}

/**
* Get file size for a file on the cloud
* Returns undefined if unable to determine size
* Return the interpreted type and size of the cloud object.
*
* Returns type = "multi-object" if the URL points to a multi-object file like Zarr
* Returns type = "webpage" if the URL points to a webpage (e.g. HTML)
* Returns type = "image" if the URL points to an image (e.g. PNG, JPEG)
* Returns type = "unknown" if the URL points to an unknown type of file
* Returns size = undefined if unable to determine size
*
* Throws an error if BFF seemingly should be able to determine size or type but fails
* to do so.
*/
public async getCloudObjectSize(url: string): Promise<number | undefined> {
public async getCloudObjectInfo(url: string): Promise<HttpInfo> {
if (isMultiObjectFile(url)) {
const cloudDirInfo = await this.getCloudDirectoryInfo(url);
if (!cloudDirInfo) return;
return cloudDirInfo.size;
} else if (url.includes("amazonaws.com")) {
// Handle individual S3 files if they are simple
return this.getHttpObjectSize(url);
const { size } = cloudDirInfo || {};
return { type: "multi-object", size };
}
// Avoid trying to parse non-http URLs or non-simple S3 URLs
if (!url.startsWith("http") || !url.includes("amazonaws.com")) {
return { type: "unknown", size: undefined };
}

return this.getHttpObjectSize(url);
}

/**
Expand Down Expand Up @@ -174,18 +191,26 @@ export default class S3StorageService extends HttpServiceBase {
}

/**
* Attempt to retrieve file size from an http object using a HEAD request.
* Attempt to retrieve object content type and size from an http object using a HEAD request.
*
* Returns bytes (octet)
* Returns size in bytes (octet) and type as one of "webpage", "image", or "unknown"
*/
private async getHttpObjectSize(url: string): Promise<number> {
private async getHttpObjectSize(url: string): Promise<HttpInfo> {
let response: AxiosResponse;
try {
const response = await axios.head(url);
return parseInt(response.headers["content-length"] || "0", 10);
response = await axios.head(url);
} catch (err) {
console.error(`Failed to get file size (content-length): ${err}`);
console.error(`Failed to get HEAD url. Unable to get content length or type: ${err}`);
throw err;
}

const contentLength = response.headers["content-length"];
const parsedLength = isNil(contentLength) ? undefined : parseInt(contentLength, 10);
const size = parsedLength && isNaN(parsedLength) ? undefined : parsedLength;
const contentType = response.headers["content-type"]?.toLowerCase() ?? "";
if (contentType.startsWith("image/")) return { type: "image", size };
if (contentType.includes("text/html")) return { type: "webpage", size };
return { type: "unknown", size };
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/core/state/interaction/logics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,8 @@ const downloadFilesLogic = createLogic({
await Promise.all(
filesToDownload.map(async (file) => {
if (!file.size) {
file.size = await s3StorageService.getCloudObjectSize(file.path);
const { size } = await s3StorageService.getCloudObjectInfo(file.path);
file.size = size;
if (file.size === undefined) someFilesHaveUnknownSize = true;
}
Comment on lines 314 to 318
})
Expand Down
Loading