~repos /website

#astro#js#html#css

git clone https://pyrossh.dev/repos/website.git

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


fa8f996a pyrossh

2 weeks ago
update commit
src/content.config.ts CHANGED
@@ -1,18 +1,8 @@
1
1
  import fs from "fs/promises";
2
- import { simpleGit } from "simple-git";
3
2
  import { glob } from 'astro/loaders';
4
3
  import { defineCollection, z } from 'astro:content';
5
4
  import { REPOS } from './consts';
6
- import { buildFileTree, sortChildren, CommitSchema, FileNodeSchema, collectCommits } from "./utils/files";
5
+ import { checkFileExists } from "./utils/files";
7
-
8
- async function checkFileExists(filePath: string) {
9
- try {
10
- await fs.stat(filePath);
11
- return true; // File exists
12
- } catch (error) {
13
- return false; // File does not exist or cannot be accessed
14
- }
15
- }
16
6
 
17
7
  export const collections = {
18
8
  repos: defineCollection({
@@ -22,28 +12,6 @@ export const collections = {
22
12
  for (const r of REPOS) {
23
13
  const repoPath = `/Users/pyrossh/Code/pyrossh/${r.title}`;
24
14
  const readmePath = repoPath + "/README.md";
25
- console.log("loading repo " + repoPath)
26
- const git = simpleGit(repoPath)
27
- const commits = await git.log(["--branches", "--tags"]);
28
- const paths = await git.raw(["ls-files"]);
29
- const files = [];
30
- for (const p of paths.split("\n").filter((p) => p.length > 0)) {
31
- try {
32
- const stat = await fs.stat(`${repoPath}/${p}`);
33
- const isLarge = stat.size > 1024 * 512;
34
- const history = isLarge ? { all: [] } : await git.log([p]);
35
- files.push({
36
- name: p,
37
- absolutePath: `${repoPath}/${p}`,
38
- ...stat,
39
- history: history.all.map(collectCommits),
40
- })
41
- } catch (e) {
42
- // Ignore files not found
43
- console.log(e);
44
- }
45
- }
46
- const fileTree = sortChildren(buildFileTree(files));
47
15
  const readmeContent = await checkFileExists(readmePath) ? await renderMarkdown(await fs.readFile(readmePath, 'utf-8')) : {
48
16
  html: '',
49
17
  metadata: {},
@@ -55,8 +23,6 @@ export const collections = {
55
23
  description: r.description,
56
24
  tags: r.tags,
57
25
  badges: r.badges || [],
58
- commits: commits.all.map(collectCommits),
59
- files: fileTree,
60
26
  absolutePath: repoPath,
61
27
  },
62
28
  rendered: readmeContent,
@@ -73,8 +39,6 @@ export const collections = {
73
39
  img: z.string(),
74
40
  alt: z.string(),
75
41
  })).optional(),
76
- commits: z.array(CommitSchema),
77
- files: z.array(FileNodeSchema),
78
42
  absolutePath: z.string(),
79
43
  }),
80
44
  }),
src/layouts/FileLayout.astro CHANGED
@@ -2,16 +2,11 @@
2
2
  import { Icon } from "astro-icon/components";
3
3
  import { type CollectionEntry } from "astro:content";
4
4
  import RepoLayout from "@/layouts/RepoLayout.astro";
5
+ import type { FileNode } from "@/utils/files";
5
6
 
6
7
  type Props = {
7
8
  repo: CollectionEntry<"repos">;
8
- file: {
9
- name: string;
10
- path: string;
11
- size: number;
9
+ file: FileNode;
12
- isDirectory: boolean;
13
- absolutePath: string;
14
- };
15
10
  };
16
11
 
17
12
  const { repo, file } = Astro.props;
@@ -26,18 +21,18 @@ const { pathname } = Astro.url;
26
21
  <div class="title">
27
22
  <span>file:</span>
28
23
  <Icon name="mdi:file" size="24px" color="hsl(0deg 75% 75%)" />
29
- <h3>{file.path}</h3>
24
+ <h3>{file.name}</h3>
30
25
  </div>
31
26
  <hr />
32
27
  <div class="nav">
33
28
  <div>
34
29
  <a
35
30
  aria-current={
36
- pathname == `/repos/${repo.id}/files/${file.path}`
31
+ pathname == `/repos/${repo.id}/files/${file.name}`
37
32
  ? "true"
38
33
  : "false"
39
34
  }
40
- href={`/repos/${repo.id}/files/${file.path}`}
35
+ href={`/repos/${repo.id}/files/${file.name}`}
41
36
  >
42
37
  Contents
43
38
  </a>
@@ -46,11 +41,11 @@ const { pathname } = Astro.url;
46
41
  <div>
47
42
  <a
48
43
  aria-current={
49
- pathname == `/repos/${repo.id}/files/${file.path}/history`
44
+ pathname == `/repos/${repo.id}/files/${file.name}/history`
50
45
  ? "true"
51
46
  : "false"
52
47
  }
53
- href={`/repos/${repo.id}/files/${file.path}/history`}
48
+ href={`/repos/${repo.id}/files/${file.name}/history`}
54
49
  >
55
50
  History
56
51
  </a>
@@ -59,11 +54,11 @@ const { pathname } = Astro.url;
59
54
  <div>
60
55
  <a
61
56
  aria-current={
62
- pathname == `/repos/${repo.id}/files/${file.path}/blame`
57
+ pathname == `/repos/${repo.id}/files/${file.name}/blame`
63
58
  ? "true"
64
59
  : "false"
65
60
  }
66
- href={`/repos/${repo.id}/files/${file.path}/blame`}
61
+ href={`/repos/${repo.id}/files/${file.name}/blame`}
67
62
  >
68
63
  Blame
69
64
  </a>
src/layouts/RepoLayout.astro CHANGED
@@ -5,7 +5,7 @@ import Layout from "@/layouts/BaseLayout.astro";
5
5
 
6
6
  type Props = {
7
7
  repo: CollectionEntry<"repos">;
8
- }
8
+ };
9
9
 
10
10
  const {
11
11
  data: { title, description, tags, badges },
@@ -39,7 +39,7 @@ const { pathname } = Astro.url;
39
39
  https://pyrossh.dev/repos/{title}.git
40
40
  </span>
41
41
  <div
42
- data-tooltip="For features and fixes, please use git patches and send them over to my email. I will try to review them and merge them as best as I can."
42
+ data-tooltip="For contributions, please use git patches and send them over by email"
43
43
  >
44
44
  <Icon
45
45
  name="mdi:information-outline"
src/pages/repos/[...repoId]/commits/[...hash]/index.astro CHANGED
@@ -2,21 +2,26 @@
2
2
  import { type CollectionEntry, getCollection } from "astro:content";
3
3
  import RepoLayout from "@/layouts/RepoLayout.astro";
4
4
  import Commit from "@/components/Commit.astro";
5
- import type { Commit as CommitType } from "../../../../../utils/files";
5
+ import type { Commit as CommitType } from "../../../../../utils/commit";
6
- import { generateHTMLDiff } from "@/utils/diff";
6
+ import { generateHTMLDiff, getCommits } from "@/utils/commit";
7
7
  import "diff2html/bundles/css/diff2html.min.css";
8
8
 
9
9
  export async function getStaticPaths() {
10
10
  const repos = await getCollection("repos");
11
+ const routes = [];
11
- return repos.flatMap((repo) => {
12
+ for (const repo of repos) {
13
+ const commits = await getCommits(repo.data.absolutePath);
12
- return repo.data.commits.map((commit) => ({
14
+ for (const commit of commits) {
15
+ routes.push({
13
- params: { repoId: repo.id, hash: commit.hash },
16
+ params: { repoId: repo.id, hash: commit.hash },
14
- props: {
17
+ props: {
15
- repo: repo,
18
+ repo: repo,
16
- commit: commit,
19
+ commit: commit,
17
- },
20
+ },
18
- }));
19
- });
21
+ });
22
+ }
23
+ }
24
+ return routes;
20
25
  }
21
26
 
22
27
  type Props = {
src/pages/repos/[...repoId]/commits/index.astro CHANGED
@@ -2,6 +2,7 @@
2
2
  import { type CollectionEntry, getCollection } from "astro:content";
3
3
  import RepoLayout from "@/layouts/RepoLayout.astro";
4
4
  import Commit from "@/components/Commit.astro";
5
+ import { getCommits } from "@/utils/commit";
5
6
 
6
7
  export async function getStaticPaths() {
7
8
  const repos = await getCollection("repos");
@@ -16,9 +17,7 @@ type Props = {
16
17
  repo: CollectionEntry<"repos">;
17
18
  }
18
19
  const { repo } = Astro.props;
19
- const {
20
- data: { commits },
20
+ const commits = await getCommits(repo.data.absolutePath);
21
- } = repo;
22
21
  ---
23
22
 
24
23
  <RepoLayout repo={repo}>
src/pages/repos/[...repoId]/files/[...file]/blame/index.astro CHANGED
@@ -1,42 +1,37 @@
1
1
  ---
2
2
  import { type CollectionEntry, getCollection } from "astro:content";
3
3
  import FileLayout from "@/layouts/FileLayout.astro";
4
+ import type { Commit as CommitType } from "@/utils/commit";
4
- import { collectFiles } from "@/utils/files";
5
+ import type { FileNode } from "@/utils/files";
6
+ import { getFileHistory, getFiles } from "@/utils/commit";
5
7
 
6
8
  export async function getStaticPaths() {
7
9
  const repos = await getCollection("repos");
10
+ const routes = [];
8
- return repos.flatMap((repo) => {
11
+ for (const repo of repos) {
9
- const allFiles = collectFiles(repo.data.files);
12
+ const files = await getFiles(repo.data.absolutePath);
10
- return allFiles.map((file) => ({
13
+ for (const file of files) {
14
+ const history = await getFileHistory(repo.data.absolutePath, file.name);
15
+ routes.push({
11
- params: { repoId: repo.id, file: file.path },
16
+ params: { repoId: repo.id, file: file.name },
12
- props: {
17
+ props: {
13
- repo: repo,
18
+ repo: repo,
14
- file: file,
19
+ file: file,
20
+ history: history,
15
- },
21
+ },
16
- }));
17
- });
22
+ });
23
+ }
24
+ }
25
+ return routes;
18
26
  }
27
+
19
28
  type Props = {
20
- repoId: CollectionEntry<"repos">;
29
+ repo: CollectionEntry<"repos">;
21
- file: {
22
- name: string;
23
- path: string;
24
- size: number;
30
+ file: FileNode;
25
- isDirectory: boolean;
31
+ history: CommitType[];
26
- absolutePath: string;
27
- history: {
28
- hash: string;
29
- message: string;
30
- body: string;
31
- branches: string[];
32
- tags: string[];
33
- }[];
34
- };
35
32
  };
36
33
 
37
- const { repo, file } = Astro.props;
34
+ const { repo, file, history } = Astro.props;
38
35
  ---
39
36
 
40
- <FileLayout repo={repo} file={file}>
37
+ <FileLayout repo={repo} file={file}> WIP: Work in Progress... </FileLayout>
41
- WIP: Work in Progress...
42
- </FileLayout>
src/pages/repos/[...repoId]/files/[...file]/history/index.astro CHANGED
@@ -2,37 +2,39 @@
2
2
  import { type CollectionEntry, getCollection } from "astro:content";
3
3
  import FileLayout from "@/layouts/FileLayout.astro";
4
4
  import Commit from "@/components/Commit.astro";
5
- import type { Commit as CommitType } from "../../../../../../utils/files";
5
+ import type { Commit as CommitType } from "@/utils/commit";
6
- import { collectFiles } from "@/utils/files";
6
+ import type { FileNode } from "@/utils/files";
7
+ import { getFileHistory, getFiles } from "@/utils/commit";
7
8
 
8
9
  export async function getStaticPaths() {
9
10
  const repos = await getCollection("repos");
11
+ const routes = [];
10
- return repos.flatMap((repo) => {
12
+ for (const repo of repos) {
11
- const allFiles = collectFiles(repo.data.files);
13
+ const files = await getFiles(repo.data.absolutePath);
12
- return allFiles.map((file) => ({
14
+ for (const file of files) {
15
+ const history = await getFileHistory(repo.data.absolutePath, file.name);
16
+ routes.push({
13
- params: { repoId: repo.id, file: file.path },
17
+ params: { repoId: repo.id, file: file.name },
14
- props: {
18
+ props: {
15
- repo: repo,
19
+ repo: repo,
16
- file: file,
20
+ file: file,
21
+ history: history,
17
- },
22
+ },
18
- }));
19
- });
23
+ });
24
+ }
25
+ }
26
+ return routes;
20
27
  }
28
+
21
29
  type Props = {
22
30
  repo: CollectionEntry<"repos">;
23
- file: {
24
- name: string;
25
- path: string;
26
- size: number;
31
+ file: FileNode;
27
- isDirectory: boolean;
28
- absolutePath: string;
29
- history: CommitType[];
32
+ history: CommitType[];
30
- };
31
33
  };
32
34
 
33
- const { repo, file } = Astro.props;
35
+ const { repo, file, history } = Astro.props;
34
36
  ---
35
37
 
36
38
  <FileLayout repo={repo} file={file}>
37
- {file.history.map((commit) => <Commit repoId={repo.id} commit={commit} />)}
39
+ {history.map((commit) => <Commit repoId={repo.id} commit={commit} />)}
38
40
  </FileLayout>
src/pages/repos/[...repoId]/files/[...file]/index.astro CHANGED
@@ -4,30 +4,30 @@ import path from "path";
4
4
  import { type CollectionEntry, getCollection } from "astro:content";
5
5
  import { Code } from "astro-expressive-code/components";
6
6
  import FileLayout from "@/layouts/FileLayout.astro";
7
- import { collectFiles } from "@/utils/files";
7
+ import { getFiles } from "@/utils/commit";
8
+ import type { FileNode } from "@/utils/files";
8
9
 
9
10
  export async function getStaticPaths() {
10
11
  const repos = await getCollection("repos");
12
+ const routes = [];
11
- return repos.flatMap((repo) => {
13
+ for (const repo of repos) {
12
- const allFiles = collectFiles(repo.data.files);
14
+ const files = await getFiles(repo.data.absolutePath);
15
+ routes.push(
13
- return allFiles.map((file) => ({
16
+ ...files.map((file) => ( {
14
- params: { repoId: repo.id, file: file.path },
17
+ params: { repoId: repo.id, file: file.name },
15
- props: {
18
+ props: {
16
- repo: repo,
19
+ repo: repo,
17
- file: file,
20
+ file: file,
18
- },
21
+ },
19
- }));
22
+ }))
20
- });
23
+ );
24
+ }
25
+ return routes;
21
26
  }
27
+
22
28
  type Props = {
23
- repoId: CollectionEntry<"repos">;
29
+ repo: CollectionEntry<"repos">;
24
- file: {
25
- name: string;
26
- path: string;
27
- size: number;
30
+ file: FileNode;
28
- isDirectory: boolean;
29
- absolutePath: string;
30
- };
31
31
  };
32
32
 
33
33
  const { repo, file } = Astro.props;
src/pages/repos/[...repoId]/files/index.astro CHANGED
@@ -2,6 +2,8 @@
2
2
  import { type CollectionEntry, getCollection } from "astro:content";
3
3
  import RepoLayout from "@/layouts/RepoLayout.astro";
4
4
  import RecursiveList from "@/components/RecursiveList.astro";
5
+ import { getFiles } from "@/utils/commit";
6
+ import { buildFileTree, sortChildren } from "@/utils/files";
5
7
 
6
8
  export async function getStaticPaths() {
7
9
  const repos = await getCollection("repos");
@@ -16,12 +18,14 @@ type Props = {
16
18
  repo: CollectionEntry<"repos">;
17
19
  };
18
20
  const { repo } = Astro.props;
21
+ const files = await getFiles(repo.data.absolutePath);
22
+ const sortedFiles = sortChildren(buildFileTree(files));
19
23
  ---
20
24
 
21
25
  <RepoLayout repo={repo}>
22
26
  <div class="container">
23
27
  <ul class="file-tree">
24
- <RecursiveList items={repo.data.files} repoId={repo.id} />
28
+ <RecursiveList items={sortedFiles} repoId={repo.id} />
25
29
  </ul>
26
30
  </div>
27
31
  </RepoLayout>
src/pages/repos/[...repoId]/index.astro CHANGED
@@ -4,6 +4,7 @@ import { render } from "astro:content";
4
4
  import RepoLayout from "@/layouts/RepoLayout.astro";
5
5
  import Commit from "@/components/Commit.astro";
6
6
  import styles from "./index.module.css";
7
+ import { getCommits } from "@/utils/commit";
7
8
 
8
9
  export async function getStaticPaths() {
9
10
  const repos = await getCollection("repos");
@@ -19,9 +20,7 @@ type Props = {
19
20
  };
20
21
 
21
22
  const { repo } = Astro.props;
22
- const {
23
- data: { commits },
23
+ const commits = await getCommits(repo.data.absolutePath);
24
- } = repo;
25
24
  const latestCommits = commits.slice(0, 3);
26
25
  const { Content } = await render(repo);
27
26
  ---
src/utils/commit.ts ADDED
@@ -0,0 +1,79 @@
1
+ import Diff2html from "diff2html";
2
+ import fs from "fs/promises";
3
+ import { simpleGit } from "simple-git";
4
+ import { type FileNode } from "./files";
5
+
6
+ export interface Commit {
7
+ hash: string;
8
+ message: string;
9
+ body: string;
10
+ author_name: string;
11
+ author_email: string;
12
+ date: string;
13
+ branches: string[];
14
+ tags: string[];
15
+ }
16
+
17
+ export const collectCommits = (item: { hash: any; message: any; body: any; author_name: any; author_email: any; date: any; refs: string; }) => ({
18
+ hash: item.hash,
19
+ message: item.message,
20
+ body: `${item.message}\n\n${item.body}`,
21
+ author_name: item.author_name,
22
+ author_email: item.author_email,
23
+ date: item.date,
24
+ branches: item.refs.split(",").filter((ref: string) => ref.includes("origin/")).map((ref) => ref.replace("origin/", "")),
25
+ tags: item.refs.split(",").filter((ref: string) => ref.includes("tag: ")).map((ref) => ref.replace("tag: ", "")),
26
+ });
27
+
28
+ export const getCommits = async (repoPath: string): Promise<Commit[]> => {
29
+ const git = simpleGit(repoPath);
30
+ const commits = await git.log(["--branches", "--tags"]);
31
+ return commits.all.map(collectCommits);
32
+ }
33
+
34
+ export const getFiles = async (repoPath: string): Promise<FileNode[]> => {
35
+ const git = simpleGit(repoPath);
36
+ const paths = await git.raw(["ls-files"]);
37
+ const files = [];
38
+ for (const p of paths.split("\n").filter((p) => p.length > 0)) {
39
+ try {
40
+ const stat = await fs.stat(`${repoPath}/${p}`);
41
+ files.push({
42
+ name: p,
43
+ absolutePath: `${repoPath}/${p}`,
44
+ ...stat,
45
+ })
46
+ } catch (e) {
47
+ // Ignore files not found
48
+ console.log(e);
49
+ }
50
+ }
51
+ return files;
52
+ }
53
+
54
+ export const getFileHistory = async (repoPath: string, filePath: string): Promise<Commit[]> => {
55
+ const git = simpleGit(repoPath);
56
+ const stat = await fs.stat(`${repoPath}/${filePath}`);
57
+ const isLarge = stat.size > 1024 * 512;
58
+ const history = isLarge ? { all: [] } : await git.log([filePath]);
59
+ return history.all.map(collectCommits);
60
+ }
61
+
62
+ export const generateHTMLDiff = async (repoPath: string, hash: string): Promise<string> => {
63
+ const git = simpleGit(repoPath);
64
+ let diff = '';
65
+ try {
66
+ diff = await git.diff([`${hash}^..${hash}`]);
67
+ } catch (e) {
68
+ diff = await git.diff([hash]);
69
+ }
70
+ if (diff.length > 1024 * 512) {
71
+ return '<p>Diff too large to display.</p>';
72
+ }
73
+ const diffJson = Diff2html.parse(diff);
74
+ const diffHtml = Diff2html.html(diffJson, {
75
+ drawFileList: true,
76
+ matching: "lines",
77
+ });
78
+ return diffHtml;
79
+ }
src/utils/diff.ts DELETED
@@ -1,13 +0,0 @@
1
- import Diff2html from "diff2html";
2
- import { simpleGit } from "simple-git";
3
-
4
- export const generateHTMLDiff = async (repoPath: string, hash: string, file?: string): Promise<string> => {
5
- const git = simpleGit(repoPath);
6
- const diff = await git.diff([`${hash}^..${hash}`].concat(file ? [file] : []));
7
- const diffJson = Diff2html.parse(diff);
8
- const diffHtml = Diff2html.html(diffJson, {
9
- drawFileList: true,
10
- matching: "lines",
11
- });
12
- return diffHtml;
13
- }
src/utils/files.ts CHANGED
@@ -1,83 +1,15 @@
1
- import { z } from 'astro:content';
1
+ import fs from "fs/promises";
2
-
3
- export interface DiffSummary {
4
- changed: number;
5
- insertions: number;
6
- deletions: number;
7
- files: string[];
8
- }
9
-
10
- export interface Commit {
11
- hash: string;
12
- message: string;
13
- body: string;
14
- author_name: string;
15
- author_email: string;
16
- date: string;
17
- branches: string[];
18
- tags: string[];
19
- }
20
2
 
21
3
  export interface FileNode {
22
4
  name: string;
23
5
  path: string;
6
+ size: number;
7
+ ext: string;
8
+ absolutePath: string;
24
9
  isDirectory: boolean;
25
10
  children?: FileNode[];
26
- size?: number;
27
- ext?: string;
28
- absolutePath?: string;
29
- history?: Commit[];
30
11
  }
31
12
 
32
- export const CommitSchema = z.object({
33
- hash: z.string(),
34
- message: z.string(),
35
- body: z.string(),
36
- author_name: z.string(),
37
- author_email: z.string(),
38
- date: z.string(),
39
- branches: z.array(z.string()),
40
- tags: z.array(z.string()),
41
- });
42
-
43
- export const FileNodeSchema: z.ZodType<FileNode> = z.lazy(() =>
44
- z.object({
45
- name: z.string(),
46
- path: z.string(),
47
- isDirectory: z.boolean(),
48
- children: z.array(FileNodeSchema),
49
- size: z.number(),
50
- ext: z.string(),
51
- absolutePath: z.string(),
52
- history: z.array(CommitSchema),
53
- })
54
- );
55
-
56
- export const collectCommits = (item: { hash: any; message: any; body: any; author_name: any; author_email: any; date: any; refs: string; }) => ({
57
- hash: item.hash,
58
- message: item.message,
59
- body: `${item.message}\n\n${item.body}`,
60
- author_name: item.author_name,
61
- author_email: item.author_email,
62
- date: item.date,
63
- branches: item.refs.split(",").filter((ref: string) => ref.includes("origin/")).map((ref) => ref.replace("origin/", "")),
64
- tags: item.refs.split(",").filter((ref: string) => ref.includes("tag: ")).map((ref) => ref.replace("tag: ", "")),
65
- });
66
-
67
- export const collectFiles = (nodes: FileNode[]): FileNode[] => {
68
- const files: FileNode[] = [];
69
-
70
- for (const node of nodes) {
71
- if (node.isDirectory && node.children) {
72
- files.push(...collectFiles(node.children));
73
- } else if (!node.isDirectory) {
74
- files.push(node);
75
- }
76
- }
77
-
78
- return files;
79
- };
80
-
81
13
  export const sortAll = (a: FileNode, b: FileNode): number => {
82
14
  if (a.isDirectory && !b.isDirectory) return -1;
83
15
  if (!a.isDirectory && b.isDirectory) return 1;
@@ -112,15 +44,11 @@ export const buildFileTree = (files: any[]): FileNode[] => {
112
44
  name: part,
113
45
  path: currentPath,
114
46
  isDirectory: !isLastPart,
47
+ size: file.size,
48
+ ext: file.ext,
49
+ absolutePath: file.absolutePath,
115
50
  };
116
-
117
- if (isLastPart) {
51
+ if (!isLastPart) {
118
- // It's a file, add file metadata
119
- newNode.size = file.size;
120
- newNode.ext = file.ext;
121
- newNode.history = file.history;
122
- newNode.absolutePath = file.absolutePath;
123
- } else {
124
52
  // It's a directory
125
53
  newNode.children = [];
126
54
  }
@@ -136,4 +64,13 @@ export const buildFileTree = (files: any[]): FileNode[] => {
136
64
  }
137
65
 
138
66
  return root;
67
+ }
68
+
69
+ export const checkFileExists = async (filePath: string) => {
70
+ try {
71
+ await fs.stat(filePath);
72
+ return true; // File exists
73
+ } catch (error) {
74
+ return false; // File does not exist or cannot be accessed
75
+ }
139
76
  }