-
-
Notifications
You must be signed in to change notification settings - Fork 246
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix(FileImageGenerator): remove
extrac-file-icon
dependency (#1075)
* Use app.getFileIcon instead of extract-file-icon * Added macos app icon extractor * Use more accurate method name * feat(FileImageGenerator): added generic app icon and folder icon extractor on macos * chore: removed unused file * feat: added WindowsApplicationIconExtractor * Added bulk icon extraction for better performance * Added Windows folder icon extractor
- Loading branch information
1 parent
845c2f1
commit f923aeb
Showing
37 changed files
with
583 additions
and
302 deletions.
There are no files selected for viewing
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import { createHash } from "crypto"; | ||
|
||
export class CacheFileNameGenerator { | ||
public generateCacheFileName(filePath: string): string { | ||
return createHash("sha1").update(filePath).digest("hex"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
import type { Image } from "@common/Core/Image"; | ||
|
||
export interface FileIconExtractor { | ||
machtes: (filePath: string) => boolean; | ||
extractFileIcon: (filePath: string) => Promise<Image>; | ||
extractFileIcons: (filePaths: string[]) => Promise<Record<string, Image>>; | ||
} |
167 changes: 92 additions & 75 deletions
167
src/main/Core/ImageGenerator/FileImageGenerator.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,89 +1,106 @@ | ||
import type { FileSystemUtility } from "@Core/FileSystemUtility"; | ||
import type { Image } from "@common/Core/Image"; | ||
import { createHash } from "crypto"; | ||
import { join } from "path"; | ||
import { describe, expect, it, vi } from "vitest"; | ||
import { describe, expect, it } from "vitest"; | ||
import { FileImageGenerator } from "./FileImageGenerator"; | ||
|
||
describe(FileImageGenerator, () => { | ||
const cacheFolderPath = "cacheFolderPath"; | ||
|
||
it("should clear cache folder if it exists", async () => { | ||
const pathExistsMock = vi.fn().mockReturnValue(true); | ||
const clearFolderMock = vi.fn().mockReturnValue(Promise.resolve()); | ||
|
||
const fileSystemUtility = <FileSystemUtility>{ | ||
pathExists: (f) => pathExistsMock(f), | ||
clearFolder: (f) => clearFolderMock(f), | ||
}; | ||
|
||
const fileImageGenerator = new FileImageGenerator(cacheFolderPath, fileSystemUtility, () => null); | ||
|
||
await fileImageGenerator.clearCache(); | ||
expect(pathExistsMock).toHaveBeenCalledWith(cacheFolderPath); | ||
expect(clearFolderMock).toHaveBeenCalledWith(cacheFolderPath); | ||
}); | ||
|
||
it("should do nothing if cache folder does not exist", async () => { | ||
const pathExistsMock = vi.fn().mockReturnValue(false); | ||
const fileSystemUtility = <FileSystemUtility>{ pathExists: (f) => pathExistsMock(f) }; | ||
|
||
const fileImageGenerator = new FileImageGenerator(cacheFolderPath, fileSystemUtility, () => null); | ||
|
||
await fileImageGenerator.clearCache(); | ||
expect(pathExistsMock).toHaveBeenCalledWith(cacheFolderPath); | ||
it("should return the extracted image from the first matching file icon extractor", async () => { | ||
const fileImageGenerator = new FileImageGenerator([ | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{ url: "test url 1" }, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{ url: "test url 2" }, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
{ | ||
machtes: () => true, | ||
extractFileIcon: async () => <Image>{ url: "test url 3" }, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
]); | ||
|
||
expect(await fileImageGenerator.getImage("my file path")).toEqual(<Image>{ url: "test url 3" }); | ||
}); | ||
|
||
it("should create the cached file if it doesn't exist and return the image", async () => { | ||
const pathExistsMock = vi.fn().mockReturnValue(false); | ||
const writePngMock = vi.fn().mockReturnValue(Promise.resolve()); | ||
const buffer = Buffer.from("testBuffer"); | ||
|
||
const fileSystemUtility = <FileSystemUtility>{ | ||
pathExists: (f) => pathExistsMock(f), | ||
writePng: (b, f) => writePngMock(b, f), | ||
}; | ||
|
||
const cachedPngFilePath = join(cacheFolderPath, `${createHash("sha1").update("my file").digest("hex")}.png`); | ||
|
||
const actual = await new FileImageGenerator(cacheFolderPath, fileSystemUtility, () => buffer).getImage( | ||
"my file", | ||
it("should throw an error if all file icon extractors don't match the given file path", async () => { | ||
const fileImageGenerator = new FileImageGenerator([ | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{ url: "test url 1" }, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{ url: "test url 2" }, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{ url: "test url 3" }, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
]); | ||
|
||
await expect(fileImageGenerator.getImage("my file path")).rejects.toThrowError( | ||
`Failed to extract file icon from path "my file path". Reason: file path did not match any file icon extractor`, | ||
); | ||
|
||
expect(actual).toEqual(<Image>{ url: `file://${cachedPngFilePath}` }); | ||
expect(pathExistsMock).toHaveBeenCalledWith(cachedPngFilePath); | ||
expect(writePngMock).toHaveBeenCalledWith(buffer, cachedPngFilePath); | ||
}); | ||
|
||
it("should not write a new cached file if it already exists and return the image", async () => { | ||
const pathExistsMock = vi.fn().mockReturnValue(true); | ||
const writePngMock = vi.fn().mockReturnValue(Promise.resolve()); | ||
|
||
const fileSystemUtility = <FileSystemUtility>{ | ||
pathExists: (f) => pathExistsMock(f), | ||
writePng: (b, f) => writePngMock(b, f), | ||
}; | ||
|
||
const cachedPngFilePath = join(cacheFolderPath, `${createHash("sha1").update("my file").digest("hex")}.png`); | ||
|
||
const actual = await new FileImageGenerator(cacheFolderPath, fileSystemUtility, () => null).getImage("my file"); | ||
|
||
expect(actual).toEqual(<Image>{ url: `file://${cachedPngFilePath}` }); | ||
expect(pathExistsMock).toHaveBeenCalledWith(cachedPngFilePath); | ||
expect(writePngMock).not.toHaveBeenCalled(); | ||
it("should bulk extract file icons", async () => { | ||
const fileImageGenerator = new FileImageGenerator([ | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{}, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{}, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
{ | ||
machtes: () => true, | ||
extractFileIcon: async () => <Image>{}, | ||
extractFileIcons: async () => | ||
<Record<string, Image>>{ | ||
path1: <Image>{ url: "test url 1" }, | ||
path2: <Image>{ url: "test url 2" }, | ||
path3: <Image>{ url: "test url 3" }, | ||
}, | ||
}, | ||
]); | ||
|
||
expect(await fileImageGenerator.getImages(["path1", "path2", "path3"])).toEqual({ | ||
path1: <Image>{ url: "test url 1" }, | ||
path2: <Image>{ url: "test url 2" }, | ||
path3: <Image>{ url: "test url 3" }, | ||
}); | ||
}); | ||
|
||
it("should throw an error if getFileIcon returns buffer with empty buffer length", async () => { | ||
const pathExistsMock = vi.fn().mockReturnValue(false); | ||
const fileSystemUtility = <FileSystemUtility>{ pathExists: (f) => pathExistsMock(f) }; | ||
|
||
const cachedPngFilePath = join(cacheFolderPath, `${createHash("sha1").update("my file").digest("hex")}.png`); | ||
|
||
const fileImageGenerator = new FileImageGenerator(cacheFolderPath, fileSystemUtility, () => Buffer.alloc(0)); | ||
|
||
expect(() => fileImageGenerator.getImage("my file")).rejects.toThrow( | ||
"getFileIcon returned Buffer with length 0", | ||
it("should throw an error when no file image extractor matches all given file paths", async () => { | ||
const fileImageGenerator = new FileImageGenerator([ | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{}, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{}, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
{ | ||
machtes: () => false, | ||
extractFileIcon: async () => <Image>{}, | ||
extractFileIcons: async () => <Record<string, Image>>{}, | ||
}, | ||
]); | ||
|
||
await expect(fileImageGenerator.getImages(["path1", "path2", "path3"])).rejects.toThrowError( | ||
"Failed to extract file icons. Reason: file paths did not match any file icon extractor", | ||
); | ||
expect(pathExistsMock).toHaveBeenCalledWith(cachedPngFilePath); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,49 +1,29 @@ | ||
import type { FileSystemUtility } from "@Core/FileSystemUtility"; | ||
import type { Image } from "@common/Core/Image"; | ||
import { createHash } from "crypto"; | ||
import { join } from "path"; | ||
import type { FileImageGenerator as FileImageGeneratorInterface } from "./Contract"; | ||
import type { FileIconExtractor } from "./FileIconExtractor"; | ||
|
||
export class FileImageGenerator implements FileImageGeneratorInterface { | ||
public constructor( | ||
private readonly cacheFolderPath: string, | ||
private readonly fileSystemUtility: FileSystemUtility, | ||
private readonly getFileIcon: (filePath: string) => Buffer, | ||
) {} | ||
|
||
public async clearCache(): Promise<void> { | ||
const exists = await this.fileSystemUtility.pathExists(this.cacheFolderPath); | ||
|
||
if (exists) { | ||
await this.fileSystemUtility.clearFolder(this.cacheFolderPath); | ||
} | ||
} | ||
public constructor(private readonly fileIconExtractors: FileIconExtractor[]) {} | ||
|
||
public async getImage(filePath: string): Promise<Image> { | ||
const cachedPngFilePath = await this.ensureCachedPngFileExists(filePath); | ||
for (const fileIconExtractor of this.fileIconExtractors) { | ||
if (fileIconExtractor.machtes(filePath)) { | ||
return await fileIconExtractor.extractFileIcon(filePath); | ||
} | ||
} | ||
|
||
return { url: `file://${cachedPngFilePath}` }; | ||
throw new Error( | ||
`Failed to extract file icon from path "${filePath}". Reason: file path did not match any file icon extractor`, | ||
); | ||
} | ||
|
||
private async ensureCachedPngFileExists(filePath: string): Promise<string> { | ||
const cachedPngFilePath = join(this.cacheFolderPath, `${this.generateCacheFileName(filePath)}.png`); | ||
|
||
const exists = await this.fileSystemUtility.pathExists(cachedPngFilePath); | ||
|
||
if (!exists) { | ||
const buffer = this.getFileIcon(filePath); | ||
|
||
if (!buffer.byteLength) { | ||
throw new Error("getFileIcon returned Buffer with length 0"); | ||
public async getImages(filePaths: string[]): Promise<Record<string, Image>> { | ||
for (const fileIconExtractor of this.fileIconExtractors) { | ||
if (filePaths.every((filePath) => fileIconExtractor.machtes(filePath))) { | ||
return await fileIconExtractor.extractFileIcons(filePaths); | ||
} | ||
|
||
await this.fileSystemUtility.writePng(buffer, cachedPngFilePath); | ||
} | ||
|
||
return cachedPngFilePath; | ||
} | ||
|
||
private generateCacheFileName(filePath: string): string { | ||
return createHash("sha1").update(filePath).digest("hex"); | ||
throw new Error(`Failed to extract file icons. Reason: file paths did not match any file icon extractor`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import type { Image } from "@common/Core/Image"; | ||
import type { App } from "electron"; | ||
import type { FileIconExtractor } from "./FileIconExtractor"; | ||
|
||
export class GenericFileIconExtractor implements FileIconExtractor { | ||
public constructor(private readonly app: App) {} | ||
|
||
public machtes() { | ||
return true; | ||
} | ||
|
||
public async extractFileIcon(filePath: string) { | ||
const nativeImage = await this.app.getFileIcon(filePath); | ||
return { url: nativeImage.toDataURL() }; | ||
} | ||
|
||
public async extractFileIcons(filePaths: string[]) { | ||
const result: Record<string, Image> = {}; | ||
|
||
const promiseResults = await Promise.allSettled(filePaths.map((f) => this.extractFileIcon(f))); | ||
|
||
for (let i = 0; i < filePaths.length; i++) { | ||
const filePath = filePaths[i]; | ||
const promiseResult = promiseResults[i]; | ||
|
||
if (promiseResult.status === "fulfilled") { | ||
result[filePath] = promiseResult.value; | ||
} | ||
} | ||
|
||
return result; | ||
} | ||
} |
Oops, something went wrong.