feat: Use relative paths for local file assets (#1027)

Esse commit está contido em:
Tanner Barlow
2020-11-04 15:25:00 -08:00
commit de GitHub
commit d48de94b8b
17 arquivos alterados com 186 adições e 33 exclusões
+9 -3
Ver Arquivo
@@ -11916,6 +11916,12 @@
} }
} }
}, },
"mock-fs": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-4.13.0.tgz",
"integrity": "sha512-DD0vOdofJdoaRNtnWcrXe6RQbpHkPPmtqGq14uRX0F8ZKJ5nv89CVTYl/BZdppDxBDaV0hl75htg3abpEWlPZA==",
"dev": true
},
"moo": { "moo": {
"version": "0.4.3", "version": "0.4.3",
"resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz", "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz",
@@ -12097,7 +12103,7 @@
"dependencies": { "dependencies": {
"semver": { "semver": {
"version": "5.3.0", "version": "5.3.0",
"resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true "dev": true
} }
@@ -12234,7 +12240,7 @@
}, },
"chalk": { "chalk": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true, "dev": true,
"requires": { "requires": {
@@ -17293,7 +17299,7 @@
"dependencies": { "dependencies": {
"source-map": { "source-map": {
"version": "0.4.4", "version": "0.4.4",
"resolved": "http://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
"integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
"dev": true, "dev": true,
"requires": { "requires": {
+1
Ver Arquivo
@@ -119,6 +119,7 @@
"foreman": "^3.0.1", "foreman": "^3.0.1",
"jest-enzyme": "^7.0.1", "jest-enzyme": "^7.0.1",
"jquery": "^3.3.1", "jquery": "^3.3.1",
"mock-fs": "^4.13.0",
"node-sass": "^4.14.1", "node-sass": "^4.14.1",
"popper.js": "^1.14.6", "popper.js": "^1.14.6",
"redux-immutable-state-invariant": "^2.1.0", "redux-immutable-state-invariant": "^2.1.0",
+1
Ver Arquivo
@@ -398,6 +398,7 @@ export default class MockFactory {
public static createLocalFileSystemOptions(): ILocalFileSystemProxyOptions { public static createLocalFileSystemOptions(): ILocalFileSystemProxyOptions {
return { return {
folderPath: "C:\\projects\\vott\\project", folderPath: "C:\\projects\\vott\\project",
relativePath: false,
}; };
} }
@@ -1,7 +1,8 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path, { relative } from "path";
import shortid from "shortid"; import shortid from "shortid";
import LocalFileSystem from "./localFileSystem"; import LocalFileSystem from "./localFileSystem";
import mockFs from "mock-fs";
jest.mock("electron", () => ({ jest.mock("electron", () => ({
dialog: { dialog: {
@@ -9,14 +10,36 @@ jest.mock("electron", () => ({
}, },
})); }));
import { dialog } from "electron"; import { dialog } from "electron";
import { AssetService } from "../../../services/assetService";
describe("LocalFileSystem Storage Provider", () => { describe("LocalFileSystem Storage Provider", () => {
let localFileSystem: LocalFileSystem = null; let localFileSystem: LocalFileSystem = null;
const sourcePath = path.join("path", "to", "my", "source");
beforeEach(() => { beforeEach(() => {
localFileSystem = new LocalFileSystem(null); localFileSystem = new LocalFileSystem(null);
}); });
beforeAll(() => {
mockFs({
path: {
to: {
my: {
source: {
"file1.jpg": "contents",
"file2.jpg": "contents",
"file3.jpg": "contents",
},
},
},
},
});
});
afterAll(() => {
mockFs.restore();
});
it("writes, reads and deletes a file as text", async () => { it("writes, reads and deletes a file as text", async () => {
const filePath = path.join(process.cwd(), "test-output", `${shortid.generate()}.json`); const filePath = path.join(process.cwd(), "test-output", `${shortid.generate()}.json`);
const contents = { const contents = {
@@ -89,4 +112,35 @@ describe("LocalFileSystem Storage Provider", () => {
it("deleting file that doesn't exist resolves successfully", async () => { it("deleting file that doesn't exist resolves successfully", async () => {
await expect(localFileSystem.deleteFile("/path/to/fake/file.txt")).resolves.not.toBeNull(); await expect(localFileSystem.deleteFile("/path/to/fake/file.txt")).resolves.not.toBeNull();
}); });
it("getAssets uses an absolute path when relative not specified", async () => {
AssetService.createAssetFromFilePath = jest.fn(() => []);
await localFileSystem.getAssets(sourcePath);
const calls: any[] = (AssetService.createAssetFromFilePath as any).mock.calls;
expect(calls).toHaveLength(3);
calls.forEach((call, index) => {
const absolutePath = path.join(sourcePath, `file${index + 1}.jpg`);
expect(call).toEqual([
absolutePath,
undefined,
absolutePath,
]);
});
});
it("getAssets uses a path relative to the source connection when specified", async () => {
AssetService.createAssetFromFilePath = jest.fn(() => []);
await localFileSystem.getAssets(sourcePath, true);
const calls: any[] = (AssetService.createAssetFromFilePath as any).mock.calls;
expect(calls).toHaveLength(3);
calls.forEach((call, index) => {
const relativePath = `file${index + 1}.jpg`;
const absolutePath = path.join(sourcePath, relativePath);
expect(call).toEqual([
absolutePath,
undefined,
relativePath,
]);
});
});
}); });
@@ -3,9 +3,10 @@ import fs from "fs";
import path from "path"; import path from "path";
import rimraf from "rimraf"; import rimraf from "rimraf";
import { IStorageProvider } from "../../../providers/storage/storageProviderFactory"; import { IStorageProvider } from "../../../providers/storage/storageProviderFactory";
import { IAsset, AssetType, StorageType } from "../../../models/applicationState"; import { IAsset, AssetType, StorageType, IConnection } from "../../../models/applicationState";
import { AssetService } from "../../../services/assetService"; import { AssetService } from "../../../services/assetService";
import { strings } from "../../../common/strings"; import { strings } from "../../../common/strings";
import { ILocalFileSystemProxyOptions } from "../../../providers/storage/localFileSystemProxy";
export default class LocalFileSystem implements IStorageProvider { export default class LocalFileSystem implements IStorageProvider {
public storageType: StorageType.Local; public storageType: StorageType.Local;
@@ -136,9 +137,12 @@ export default class LocalFileSystem implements IStorageProvider {
}); });
} }
public async getAssets(folderPath?: string): Promise<IAsset[]> { public async getAssets(sourceConnectionFolderPath?: string, relativePath: boolean = false): Promise<IAsset[]> {
return (await this.listFiles(path.normalize(folderPath))) return (await this.listFiles(path.normalize(sourceConnectionFolderPath)))
.map((filePath) => AssetService.createAssetFromFilePath(filePath)) .map((filePath) => AssetService.createAssetFromFilePath(
filePath,
undefined,
relativePath ? path.relative(sourceConnectionFolderPath, filePath) : filePath))
.filter((asset) => asset.type !== AssetType.Unknown); .filter((asset) => asset.type !== AssetType.Unknown);
} }
@@ -18,7 +18,7 @@ describe("Load default model from filesystem with TF io.IOHandler", () => {
return Promise.resolve([]); return Promise.resolve([]);
}); });
const handler = new ElectronProxyHandler("folder"); const handler = new ElectronProxyHandler("folder", false);
try { try {
const model = await tf.loadGraphModel(handler); const model = await tf.loadGraphModel(handler);
} catch (_) { } catch (_) {
@@ -4,8 +4,8 @@ import { LocalFileSystemProxy, ILocalFileSystemProxyOptions } from "../../provid
export class ElectronProxyHandler implements tfc.io.IOHandler { export class ElectronProxyHandler implements tfc.io.IOHandler {
protected readonly provider: LocalFileSystemProxy; protected readonly provider: LocalFileSystemProxy;
constructor(folderPath: string) { constructor(folderPath: string, relativePath: boolean) {
const options: ILocalFileSystemProxyOptions = { folderPath }; const options: ILocalFileSystemProxyOptions = { folderPath, relativePath };
this.provider = new LocalFileSystemProxy(options); this.provider = new LocalFileSystemProxy(options);
} }
+1 -1
Ver Arquivo
@@ -53,7 +53,7 @@ export class ObjectDetection {
const response = await axios.get(modelFolderPath + "/classes.json"); const response = await axios.get(modelFolderPath + "/classes.json");
this.jsonClasses = JSON.parse(JSON.stringify(response.data)); this.jsonClasses = JSON.parse(JSON.stringify(response.data));
} else { } else {
const handler = new ElectronProxyHandler(modelFolderPath); const handler = new ElectronProxyHandler(modelFolderPath, false);
this.model = await tf.loadGraphModel(handler); this.model = await tf.loadGraphModel(handler);
this.jsonClasses = await handler.loadClasses(); this.jsonClasses = await handler.loadClasses();
} }
@@ -25,7 +25,7 @@ class TestAssetProvider implements IAssetProvider {
public initialize(): Promise<void> { public initialize(): Promise<void> {
throw new Error("Method not implemented"); throw new Error("Method not implemented");
} }
public getAssets(containerName?: string): Promise<IAsset[]> { public getAssets(): Promise<IAsset[]> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
} }
@@ -10,6 +10,7 @@ import getHostProcess, { HostProcessType } from "../../common/hostProcess";
export interface IAssetProvider { export interface IAssetProvider {
initialize?(): Promise<void>; initialize?(): Promise<void>;
getAssets(containerName?: string): Promise<IAsset[]>; getAssets(containerName?: string): Promise<IAsset[]>;
addDefaultPropsToNewConnection?(connection: IConnection): IConnection;
} }
/** /**
+2 -2
Ver Arquivo
@@ -191,8 +191,8 @@ export class AzureBlobStorage implements IStorageProvider {
* @param containerName - Container from which to retrieve assets. Defaults to * @param containerName - Container from which to retrieve assets. Defaults to
* container specified in Azure Cloud Storage options * container specified in Azure Cloud Storage options
*/ */
public async getAssets(containerName?: string): Promise<IAsset[]> { public async getAssets(): Promise<IAsset[]> {
containerName = (containerName) ? containerName : this.options.containerName; const { containerName } = this.options;
const files = await this.listFiles(containerName); const files = await this.listFiles(containerName);
const result: IAsset[] = []; const result: IAsset[] = [];
for (const file of files) { for (const file of files) {
@@ -2,6 +2,7 @@ import { IpcRendererProxy } from "../../common/ipcRendererProxy";
import { LocalFileSystemProxy, ILocalFileSystemProxyOptions } from "./localFileSystemProxy"; import { LocalFileSystemProxy, ILocalFileSystemProxyOptions } from "./localFileSystemProxy";
import { StorageProviderFactory } from "./storageProviderFactory"; import { StorageProviderFactory } from "./storageProviderFactory";
import registerProviders from "../../registerProviders"; import registerProviders from "../../registerProviders";
import MockFactory from "../../common/mockFactory";
describe("LocalFileSystem Proxy Storage Provider", () => { describe("LocalFileSystem Proxy Storage Provider", () => {
it("Provider is registered with the StorageProviderFactory", () => { it("Provider is registered with the StorageProviderFactory", () => {
@@ -19,6 +20,7 @@ describe("LocalFileSystem Proxy Storage Provider", () => {
let provider: LocalFileSystemProxy = null; let provider: LocalFileSystemProxy = null;
const options: ILocalFileSystemProxyOptions = { const options: ILocalFileSystemProxyOptions = {
folderPath: "/test", folderPath: "/test",
relativePath: false,
}; };
beforeEach(() => { beforeEach(() => {
@@ -122,5 +124,40 @@ describe("LocalFileSystem Proxy Storage Provider", () => {
expect(IpcRendererProxy.send).toBeCalledWith("LocalFileSystem:listContainers", [expectedContainerPath]); expect(IpcRendererProxy.send).toBeCalledWith("LocalFileSystem:listContainers", [expectedContainerPath]);
expect(actualFolders).toEqual(expectedFolders); expect(actualFolders).toEqual(expectedFolders);
}); });
it("sends relative path argument according to options", async () => {
const sendFunction = jest.fn();
IpcRendererProxy.send = sendFunction;
await provider.getAssets();
const { folderPath, relativePath } = options;
expect(IpcRendererProxy.send).toBeCalledWith("LocalFileSystem:getAssets", [folderPath, relativePath]);
sendFunction.mockReset();
const newFolderPath = "myFolder";
const newRelativePath = true;
const relativeProvider = new LocalFileSystemProxy({
folderPath: newFolderPath,
relativePath: newRelativePath,
});
await relativeProvider.getAssets();
expect(IpcRendererProxy.send).toBeCalledWith("LocalFileSystem:getAssets", [newFolderPath, newRelativePath]);
});
it("adds default props to a new connection", () => {
const connection = MockFactory.createTestConnection();
delete connection.providerOptions["relativePath"];
expect(connection).not.toHaveProperty("providerOptions.relativePath");
delete connection.id;
expect(provider.addDefaultPropsToNewConnection(connection))
.toHaveProperty("providerOptions.relativePath", true);
});
it("does not add default props to existing connection", () => {
const connection = MockFactory.createTestConnection();
delete connection.providerOptions["relativePath"];
expect(provider.addDefaultPropsToNewConnection(connection))
.not.toHaveProperty("providerOptions.relativePath");
});
}); });
}); });
+24 -4
Ver Arquivo
@@ -1,7 +1,7 @@
import { IpcRendererProxy } from "../../common/ipcRendererProxy"; import { IpcRendererProxy } from "../../common/ipcRendererProxy";
import { IStorageProvider } from "./storageProviderFactory"; import { IStorageProvider } from "./storageProviderFactory";
import { IAssetProvider } from "./assetProviderFactory"; import { IAssetProvider } from "./assetProviderFactory";
import { IAsset, StorageType } from "../../models/applicationState"; import { IAsset, IConnection, StorageType } from "../../models/applicationState";
const PROXY_NAME = "LocalFileSystem"; const PROXY_NAME = "LocalFileSystem";
@@ -11,6 +11,7 @@ const PROXY_NAME = "LocalFileSystem";
*/ */
export interface ILocalFileSystemProxyOptions { export interface ILocalFileSystemProxyOptions {
folderPath: string; folderPath: string;
relativePath: boolean;
} }
/** /**
@@ -26,6 +27,7 @@ export class LocalFileSystemProxy implements IStorageProvider, IAssetProvider {
if (!this.options) { if (!this.options) {
this.options = { this.options = {
folderPath: null, folderPath: null,
relativePath: false,
}; };
} }
} }
@@ -125,8 +127,26 @@ export class LocalFileSystemProxy implements IStorageProvider, IAssetProvider {
* Retrieve assets from directory * Retrieve assets from directory
* @param folderName - Directory containing assets * @param folderName - Directory containing assets
*/ */
public getAssets(folderName?: string): Promise<IAsset[]> { public getAssets(): Promise<IAsset[]> {
const folderPath = [this.options.folderPath, folderName].join("/"); const { folderPath, relativePath } = this.options;
return IpcRendererProxy.send(`${PROXY_NAME}:getAssets`, [folderPath]); return IpcRendererProxy.send(`${PROXY_NAME}:getAssets`, [folderPath, relativePath]);
}
/**
* Adds default properties to new connections
*
* Currently adds `relativePath: true` to the providerOptions. Pre-existing connections
* will only use absolute path
*
* @param connection Connection
*/
public addDefaultPropsToNewConnection(connection: IConnection): IConnection {
return connection.id ? connection : {
...connection,
providerOptions: {
...connection.providerOptions,
relativePath: true,
} as any,
};
} }
} }
@@ -51,7 +51,7 @@ class TestStorageProvider implements IStorageProvider {
public deleteContainer(folderPath: string): Promise<void> { public deleteContainer(folderPath: string): Promise<void> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
public getAssets(containerName?: string): Promise<IAsset[]> { public getAssets(): Promise<IAsset[]> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
} }
@@ -11,6 +11,7 @@ import ConnectionForm from "./connectionForm";
import ConnectionItem from "./connectionItem"; import ConnectionItem from "./connectionItem";
import "./connectionsPage.scss"; import "./connectionsPage.scss";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { AssetProviderFactory } from "../../../../providers/storage/assetProviderFactory";
/** /**
* Properties for Connection Page * Properties for Connection Page
@@ -134,12 +135,20 @@ export default class ConnectionPage extends React.Component<IConnectionPageProps
} }
private onFormSubmit = async (connection: IConnection) => { private onFormSubmit = async (connection: IConnection) => {
connection = this.addDefaultPropsIfNewConnection(connection);
await this.props.actions.saveConnection(connection); await this.props.actions.saveConnection(connection);
toast.success(interpolate(strings.connections.messages.saveSuccess, { connection })); toast.success(interpolate(strings.connections.messages.saveSuccess, { connection }));
this.props.history.goBack(); this.props.history.goBack();
} }
private addDefaultPropsIfNewConnection(connection: IConnection): IConnection {
const assetProvider = AssetProviderFactory.createFromConnection(connection);
return !connection.id && assetProvider.addDefaultPropsToNewConnection
? assetProvider.addDefaultPropsToNewConnection(connection)
: connection;
}
private onFormCancel() { private onFormCancel() {
this.props.history.goBack(); this.props.history.goBack();
} }
+17
Ver Arquivo
@@ -9,6 +9,7 @@ import HtmlFileReader from "../common/htmlFileReader";
import { encodeFileURI } from "../common/utils"; import { encodeFileURI } from "../common/utils";
import _ from "lodash"; import _ from "lodash";
import registerMixins from "../registerMixins"; import registerMixins from "../registerMixins";
import MD5 from "md5.js";
describe("Asset Service", () => { describe("Asset Service", () => {
describe("Static Methods", () => { describe("Static Methods", () => {
@@ -24,6 +25,22 @@ describe("Asset Service", () => {
expect(asset.format).toEqual("jpg"); expect(asset.format).toEqual("jpg");
}); });
it("creates an asset using file path as identifier", () => {
const path = "c:/dir1/dir2/asset1.jpg";
const asset = AssetService.createAssetFromFilePath(path);
const expectedIdenfifier = `file:${path}`;
const expectedId = new MD5().update(expectedIdenfifier).digest("hex");
expect(asset.id).toEqual(expectedId);
});
it("creates an asset using passed in identifier", () => {
const path = "C:\\dir1\\dir2\\asset1.jpg";
const identifier = "asset1.jpg";
const asset = AssetService.createAssetFromFilePath(path, undefined, identifier);
const expectedId = new MD5().update(identifier).digest("hex");
expect(asset.id).toEqual(expectedId);
});
it("creates an asset from an encoded file", () => { it("creates an asset from an encoded file", () => {
const path = "C:\\dir1\\dir2\\asset%201.jpg"; const path = "C:\\dir1\\dir2\\asset%201.jpg";
const asset = AssetService.createAssetFromFilePath(path); const asset = AssetService.createAssetFromFilePath(path);
+16 -13
Ver Arquivo
@@ -23,13 +23,15 @@ export class AssetService {
/** /**
* Create IAsset from filePath * Create IAsset from filePath
* @param filePath - filepath of asset * @param assetFilePath - filepath of asset
* @param fileName - name of asset * @param assetFileName - name of asset
*/ */
public static createAssetFromFilePath(filePath: string, fileName?: string): IAsset { public static createAssetFromFilePath(
Guard.empty(filePath); assetFilePath: string,
assetFileName?: string,
const normalizedPath = filePath.toLowerCase(); assetIdentifier?: string): IAsset {
Guard.empty(assetFilePath);
const normalizedPath = assetFilePath.toLowerCase();
// If the path is not already prefixed with a protocol // If the path is not already prefixed with a protocol
// then assume it comes from the local file system // then assume it comes from the local file system
@@ -37,17 +39,18 @@ export class AssetService {
!normalizedPath.startsWith("https://") && !normalizedPath.startsWith("https://") &&
!normalizedPath.startsWith("file:")) { !normalizedPath.startsWith("file:")) {
// First replace \ character with / the do the standard url encoding then encode unsupported characters // First replace \ character with / the do the standard url encoding then encode unsupported characters
filePath = encodeFileURI(filePath, true); assetFilePath = encodeFileURI(assetFilePath, true);
} }
assetIdentifier = assetIdentifier || assetFilePath;
const md5Hash = new MD5().update(filePath).digest("hex"); const md5Hash = new MD5().update(assetIdentifier).digest("hex");
const pathParts = filePath.split(/[\\\/]/); const pathParts = assetFilePath.split(/[\\\/]/);
// Example filename: video.mp4#t=5 // Example filename: video.mp4#t=5
// fileNameParts[0] = "video" // fileNameParts[0] = "video"
// fileNameParts[1] = "mp4" // fileNameParts[1] = "mp4"
// fileNameParts[2] = "t=5" // fileNameParts[2] = "t=5"
fileName = fileName || pathParts[pathParts.length - 1]; assetFileName = assetFileName || pathParts[pathParts.length - 1];
const fileNameParts = fileName.split("."); const fileNameParts = assetFileName.split(".");
const extensionParts = fileNameParts[fileNameParts.length - 1].split(/[\?#]/); const extensionParts = fileNameParts[fileNameParts.length - 1].split(/[\?#]/);
const assetFormat = extensionParts[0]; const assetFormat = extensionParts[0];
@@ -58,8 +61,8 @@ export class AssetService {
format: assetFormat, format: assetFormat,
state: AssetState.NotVisited, state: AssetState.NotVisited,
type: assetType, type: assetType,
name: fileName, name: assetFileName,
path: filePath, path: assetFilePath,
size: null, size: null,
}; };
} }