Replace entire codebase with homarr-labs/homarr
This commit is contained in:
4
packages/icons/eslint.config.js
Normal file
4
packages/icons/eslint.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import baseConfig from "@homarr/eslint-config/base";
|
||||
|
||||
/** @type {import('typescript-eslint').Config} */
|
||||
export default [...baseConfig];
|
||||
3
packages/icons/index.ts
Normal file
3
packages/icons/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./src/icons-fetcher";
|
||||
export * from "./src/types";
|
||||
export * from "./src/auto-icon-searcher";
|
||||
37
packages/icons/package.json
Normal file
37
packages/icons/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@homarr/icons",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./index.ts",
|
||||
"./local": "./src/local.ts"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "rm -rf .turbo node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@homarr/prettier-config",
|
||||
"dependencies": {
|
||||
"@homarr/common": "workspace:^0.1.0",
|
||||
"@homarr/core": "workspace:^0.1.0",
|
||||
"@homarr/db": "workspace:^0.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@homarr/eslint-config": "workspace:^0.2.0",
|
||||
"@homarr/prettier-config": "workspace:^0.1.0",
|
||||
"@homarr/tsconfig": "workspace:^0.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
9
packages/icons/src/auto-icon-searcher.ts
Normal file
9
packages/icons/src/auto-icon-searcher.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import type { Database } from "@homarr/db";
|
||||
import { like } from "@homarr/db";
|
||||
import { icons } from "@homarr/db/schema";
|
||||
|
||||
export const getIconForName = (db: Database, name: string) => {
|
||||
return db.query.icons.findFirst({
|
||||
where: like(icons.name, `%${name}%`),
|
||||
});
|
||||
};
|
||||
52
packages/icons/src/icons-fetcher.ts
Normal file
52
packages/icons/src/icons-fetcher.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { GitHubIconRepository } from "./repositories/github.icon-repository";
|
||||
import { JsdelivrIconRepository } from "./repositories/jsdelivr.icon-repository";
|
||||
import { LocalIconRepository } from "./repositories/local.icon-repository";
|
||||
import type { RepositoryIconGroup } from "./types";
|
||||
|
||||
const repositories = [
|
||||
new GitHubIconRepository(
|
||||
"Dashboard Icons",
|
||||
"homarr-labs/dashboard-icons",
|
||||
undefined,
|
||||
new URL("https://github.com/homarr-labs/dashboard-icons"),
|
||||
new URL("https://api.github.com/repos/homarr-labs/dashboard-icons/git/trees/main?recursive=true"),
|
||||
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/{0}",
|
||||
),
|
||||
new GitHubIconRepository(
|
||||
"selfh.st",
|
||||
"selfhst/icons",
|
||||
"CC0-1.0",
|
||||
new URL("https://github.com/selfhst/icons"),
|
||||
new URL("https://api.github.com/repos/selfhst/icons/git/trees/main?recursive=true"),
|
||||
"https://cdn.jsdelivr.net/gh/selfhst/icons/{0}",
|
||||
),
|
||||
new GitHubIconRepository(
|
||||
"SimpleIcons",
|
||||
"simple-icons/simple-icons",
|
||||
"CC0-1.0",
|
||||
new URL("https://github.com/simple-icons/simple-icons"),
|
||||
new URL("https://api.github.com/repos/simple-icons/simple-icons/git/trees/master?recursive=true"),
|
||||
"https://cdn.simpleicons.org/{1}",
|
||||
),
|
||||
new JsdelivrIconRepository(
|
||||
"Papirus",
|
||||
"PapirusDevelopmentTeam/papirus-icon-theme",
|
||||
"GPL-3.0",
|
||||
new URL("https://github.com/PapirusDevelopmentTeam/papirus-icon-theme"),
|
||||
new URL("https://data.jsdelivr.com/v1/packages/gh/PapirusDevelopmentTeam/papirus_icons@master?structure=flat"),
|
||||
"https://cdn.jsdelivr.net/gh/PapirusDevelopmentTeam/papirus_icons/{0}",
|
||||
),
|
||||
new JsdelivrIconRepository(
|
||||
"Homelab SVG assets",
|
||||
"loganmarchione/homelab-svg-assets",
|
||||
"MIT",
|
||||
new URL("https://github.com/loganmarchione/homelab-svg-assets"),
|
||||
new URL("https://data.jsdelivr.com/v1/packages/gh/loganmarchione/homelab-svg-assets@main?structure=flat"),
|
||||
"https://cdn.jsdelivr.net/gh/loganmarchione/homelab-svg-assets/{0}",
|
||||
),
|
||||
new LocalIconRepository(),
|
||||
];
|
||||
|
||||
export const fetchIconsAsync = async (): Promise<RepositoryIconGroup[]> => {
|
||||
return await Promise.all(repositories.map(async (repository) => await repository.getAllIconsAsync()));
|
||||
};
|
||||
1
packages/icons/src/local.ts
Normal file
1
packages/icons/src/local.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { createLocalImageUrl, mapMediaToIcon, LOCAL_ICON_REPOSITORY_SLUG } from "./repositories/local.icon-repository";
|
||||
68
packages/icons/src/repositories/github.icon-repository.ts
Normal file
68
packages/icons/src/repositories/github.icon-repository.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { parse } from "path";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
||||
|
||||
import type { IconRepositoryLicense } from "../types/icon-repository-license";
|
||||
import type { RepositoryIconGroup } from "../types/repository-icon-group";
|
||||
import { IconRepository } from "./icon-repository";
|
||||
|
||||
export class GitHubIconRepository extends IconRepository {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly slug: string,
|
||||
public readonly license: IconRepositoryLicense,
|
||||
public readonly repositoryUrl?: URL,
|
||||
public readonly repositoryIndexingUrl?: URL,
|
||||
public readonly repositoryBlobUrlTemplate?: string,
|
||||
) {
|
||||
super(name, slug, license, repositoryUrl, repositoryIndexingUrl, repositoryBlobUrlTemplate);
|
||||
}
|
||||
|
||||
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
|
||||
const url = this.repositoryIndexingUrl;
|
||||
if (!url || !this.repositoryBlobUrlTemplate) {
|
||||
throw new Error("Repository URLs are required for this repository");
|
||||
}
|
||||
|
||||
const response = await withTimeoutAsync(async (signal) => fetchWithTrustedCertificatesAsync(url, { signal }));
|
||||
const listOfFiles = (await response.json()) as GitHubApiResponse;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
icons: listOfFiles.tree
|
||||
.filter(({ path }) =>
|
||||
this.allowedImageFileTypes.some((allowedImageFileType) => parse(path).ext === allowedImageFileType),
|
||||
)
|
||||
.map(({ path, size: sizeInBytes, sha: checksum }) => {
|
||||
const file = parse(path);
|
||||
const fileNameWithExtension = file.base;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const imageUrl = this.repositoryBlobUrlTemplate!.replace("{0}", path).replace("{1}", file.name);
|
||||
return {
|
||||
imageUrl,
|
||||
fileNameWithExtension,
|
||||
local: false,
|
||||
sizeInBytes,
|
||||
checksum,
|
||||
};
|
||||
}),
|
||||
slug: this.slug,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface GitHubApiResponse {
|
||||
sha: string;
|
||||
url: string;
|
||||
tree: TreeItem[];
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface TreeItem {
|
||||
path: string;
|
||||
mode: string;
|
||||
sha: string;
|
||||
url: string;
|
||||
size?: number;
|
||||
}
|
||||
37
packages/icons/src/repositories/icon-repository.ts
Normal file
37
packages/icons/src/repositories/icon-repository.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { createLogger } from "@homarr/core/infrastructure/logs";
|
||||
import { ErrorWithMetadata } from "@homarr/core/infrastructure/logs/error";
|
||||
|
||||
import type { IconRepositoryLicense } from "../types/icon-repository-license";
|
||||
import type { RepositoryIconGroup } from "../types/repository-icon-group";
|
||||
|
||||
const logger = createLogger({ module: "iconRepository" });
|
||||
|
||||
export abstract class IconRepository {
|
||||
protected readonly allowedImageFileTypes = [".png", ".svg", ".jpeg"];
|
||||
|
||||
protected constructor(
|
||||
public readonly name: string,
|
||||
public readonly slug: string,
|
||||
public readonly license: IconRepositoryLicense,
|
||||
public readonly repositoryUrl?: URL,
|
||||
public readonly repositoryIndexingUrl?: URL,
|
||||
public readonly repositoryBlobUrlTemplate?: string,
|
||||
) {}
|
||||
|
||||
public async getAllIconsAsync(): Promise<RepositoryIconGroup> {
|
||||
try {
|
||||
return await this.getAllIconsInternalAsync();
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
new ErrorWithMetadata("Unable to request icons from repository", { slug: this.slug }, { cause: err }),
|
||||
);
|
||||
return {
|
||||
success: false,
|
||||
icons: [],
|
||||
slug: this.slug,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract getAllIconsInternalAsync(): Promise<RepositoryIconGroup>;
|
||||
}
|
||||
59
packages/icons/src/repositories/jsdelivr.icon-repository.ts
Normal file
59
packages/icons/src/repositories/jsdelivr.icon-repository.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { parse } from "path";
|
||||
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
import { withTimeoutAsync } from "@homarr/core/infrastructure/http/timeout";
|
||||
|
||||
import type { IconRepositoryLicense } from "../types/icon-repository-license";
|
||||
import type { RepositoryIconGroup } from "../types/repository-icon-group";
|
||||
import { IconRepository } from "./icon-repository";
|
||||
|
||||
export class JsdelivrIconRepository extends IconRepository {
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly slug: string,
|
||||
public readonly license: IconRepositoryLicense,
|
||||
public readonly repositoryUrl: URL,
|
||||
public readonly repositoryIndexingUrl: URL,
|
||||
public readonly repositoryBlobUrlTemplate: string,
|
||||
) {
|
||||
super(name, slug, license, repositoryUrl, repositoryIndexingUrl, repositoryBlobUrlTemplate);
|
||||
}
|
||||
|
||||
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
|
||||
const response = await withTimeoutAsync(async (signal) =>
|
||||
fetchWithTrustedCertificatesAsync(this.repositoryIndexingUrl, { signal }),
|
||||
);
|
||||
const listOfFiles = (await response.json()) as JsdelivrApiResponse;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
icons: listOfFiles.files
|
||||
.filter(({ name: path }) =>
|
||||
this.allowedImageFileTypes.some((allowedImageFileType) => parse(path).ext === allowedImageFileType),
|
||||
)
|
||||
.map(({ name: path, size: sizeInBytes, hash: checksum }) => {
|
||||
const file = parse(path);
|
||||
const fileNameWithExtension = file.base;
|
||||
|
||||
return {
|
||||
imageUrl: this.repositoryBlobUrlTemplate.replace("{0}", path).replace("{1}", file.name),
|
||||
fileNameWithExtension,
|
||||
local: false,
|
||||
sizeInBytes,
|
||||
checksum,
|
||||
};
|
||||
}),
|
||||
slug: this.slug,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface JsdelivrApiResponse {
|
||||
files: JsdelivrFile[];
|
||||
}
|
||||
|
||||
interface JsdelivrFile {
|
||||
name: string;
|
||||
size: number;
|
||||
hash: string;
|
||||
}
|
||||
36
packages/icons/src/repositories/local.icon-repository.ts
Normal file
36
packages/icons/src/repositories/local.icon-repository.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { createHash } from "crypto";
|
||||
|
||||
import type { InferSelectModel } from "@homarr/db";
|
||||
import { db } from "@homarr/db";
|
||||
import type { medias } from "@homarr/db/schema";
|
||||
|
||||
import type { RepositoryIcon, RepositoryIconGroup } from "../types";
|
||||
import { IconRepository } from "./icon-repository";
|
||||
|
||||
export const LOCAL_ICON_REPOSITORY_SLUG = "local";
|
||||
|
||||
export class LocalIconRepository extends IconRepository {
|
||||
constructor() {
|
||||
super("Local", LOCAL_ICON_REPOSITORY_SLUG, undefined, undefined, undefined, undefined);
|
||||
}
|
||||
protected async getAllIconsInternalAsync(): Promise<RepositoryIconGroup> {
|
||||
const medias = await db.query.medias.findMany();
|
||||
return {
|
||||
success: true,
|
||||
icons: medias.map(mapMediaToIcon),
|
||||
slug: LOCAL_ICON_REPOSITORY_SLUG,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const createLocalImageUrl = (id: string) => `/api/user-medias/${id}`;
|
||||
|
||||
export const mapMediaToIcon = (
|
||||
media: Pick<InferSelectModel<typeof medias>, "name" | "id" | "content" | "size">,
|
||||
): RepositoryIcon => ({
|
||||
local: true,
|
||||
fileNameWithExtension: media.name,
|
||||
imageUrl: createLocalImageUrl(media.id),
|
||||
checksum: createHash("md5").update(media.content).digest("hex"),
|
||||
sizeInBytes: media.size,
|
||||
});
|
||||
1
packages/icons/src/types/icon-repository-license.ts
Normal file
1
packages/icons/src/types/icon-repository-license.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type IconRepositoryLicense = "MIT" | "GPL-3.0" | "CC0-1.0" | undefined;
|
||||
3
packages/icons/src/types/index.ts
Normal file
3
packages/icons/src/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./icon-repository-license";
|
||||
export * from "./repository-icon-group";
|
||||
export * from "./repository-icon";
|
||||
7
packages/icons/src/types/repository-icon-group.ts
Normal file
7
packages/icons/src/types/repository-icon-group.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { RepositoryIcon } from "./repository-icon";
|
||||
|
||||
export interface RepositoryIconGroup {
|
||||
icons: RepositoryIcon[];
|
||||
success: boolean;
|
||||
slug: string;
|
||||
}
|
||||
7
packages/icons/src/types/repository-icon.ts
Normal file
7
packages/icons/src/types/repository-icon.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface RepositoryIcon {
|
||||
fileNameWithExtension: string;
|
||||
sizeInBytes?: number;
|
||||
imageUrl: string;
|
||||
local: boolean;
|
||||
checksum: string;
|
||||
}
|
||||
8
packages/icons/tsconfig.json
Normal file
8
packages/icons/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@homarr/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user