2023-03-03 15:23:46 +00:00
|
|
|
import "reflect-metadata";
|
|
|
|
import { inject, injectable } from "tsyringe";
|
2023-10-19 17:21:17 +00:00
|
|
|
|
2023-11-07 23:27:52 -05:00
|
|
|
import crypto from "node:crypto";
|
2023-11-13 11:14:58 -05:00
|
|
|
import fs from "node:fs";
|
2023-11-07 23:27:52 -05:00
|
|
|
import path, { resolve } from "node:path";
|
2023-11-13 11:14:58 -05:00
|
|
|
import { promisify } from "node:util";
|
2023-11-13 11:43:37 -05:00
|
|
|
import { IAsyncQueue } from "@spt-aki/models/spt/utils/IAsyncQueue";
|
|
|
|
import { writeFileSync } from "atomically";
|
2023-11-07 23:27:52 -05:00
|
|
|
import lockfile from "proper-lockfile";
|
2023-03-03 15:23:46 +00:00
|
|
|
|
|
|
|
@injectable()
|
2023-11-07 23:29:25 -05:00
|
|
|
export class VFS
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
accessFilePromisify: (path: fs.PathLike, mode?: number) => Promise<void>;
|
|
|
|
copyFilePromisify: (src: fs.PathLike, dst: fs.PathLike, flags?: number) => Promise<void>;
|
2023-11-13 11:14:58 -05:00
|
|
|
mkdirPromisify: (path: fs.PathLike, options: fs.MakeDirectoryOptions & {recursive: true;}) => Promise<string>;
|
2023-03-03 15:23:46 +00:00
|
|
|
readFilePromisify: (path: fs.PathLike) => Promise<Buffer>;
|
|
|
|
writeFilePromisify: (path: fs.PathLike, data: string, options?: any) => Promise<void>;
|
2023-11-13 11:14:58 -05:00
|
|
|
readdirPromisify: (
|
|
|
|
path: fs.PathLike,
|
|
|
|
options?: BufferEncoding | {encoding: BufferEncoding; withFileTypes?: false;},
|
|
|
|
) => Promise<string[]>;
|
|
|
|
statPromisify: (path: fs.PathLike, options?: fs.StatOptions & {bigint?: false;}) => Promise<fs.Stats>;
|
2023-03-03 15:23:46 +00:00
|
|
|
unlinkPromisify: (path: fs.PathLike) => Promise<void>;
|
|
|
|
rmdirPromisify: (path: fs.PathLike) => Promise<void>;
|
2023-10-10 11:03:20 +00:00
|
|
|
renamePromisify: (oldPath: fs.PathLike, newPath: fs.PathLike) => Promise<void>;
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2023-11-13 12:31:52 -05:00
|
|
|
constructor(@inject("AsyncQueue") protected asyncQueue: IAsyncQueue)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.accessFilePromisify = promisify(fs.access);
|
|
|
|
this.copyFilePromisify = promisify(fs.copyFile);
|
|
|
|
this.mkdirPromisify = promisify(fs.mkdir);
|
|
|
|
this.readFilePromisify = promisify(fs.readFile);
|
|
|
|
this.writeFilePromisify = promisify(fs.writeFile);
|
|
|
|
this.readdirPromisify = promisify(fs.readdir);
|
|
|
|
this.statPromisify = promisify(fs.stat);
|
|
|
|
this.unlinkPromisify = promisify(fs.unlinkSync);
|
|
|
|
this.rmdirPromisify = promisify(fs.rmdir);
|
2023-10-10 11:03:20 +00:00
|
|
|
this.renamePromisify = promisify(fs.renameSync);
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public exists(filepath: fs.PathLike): boolean
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
return fs.existsSync(filepath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async existsAsync(filepath: fs.PathLike): Promise<boolean>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-11-07 23:29:25 -05:00
|
|
|
try
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
// Create the command to add to the queue
|
2023-11-13 12:31:52 -05:00
|
|
|
const command = {uuid: crypto.randomUUID(), cmd: async () => await this.accessFilePromisify(filepath)};
|
2023-03-03 15:23:46 +00:00
|
|
|
// Wait for the command completion
|
|
|
|
await this.asyncQueue.waitFor(command);
|
|
|
|
|
|
|
|
// If no Exception, the file exists
|
|
|
|
return true;
|
|
|
|
}
|
2023-11-07 23:29:25 -05:00
|
|
|
catch
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-11-07 23:29:25 -05:00
|
|
|
// If Exception, the file does not exist
|
2023-03-03 15:23:46 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public copyFile(filepath: fs.PathLike, target: fs.PathLike): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
fs.copyFileSync(filepath, target);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async copyAsync(filepath: fs.PathLike, target: fs.PathLike): Promise<void>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-11-13 12:31:52 -05:00
|
|
|
const command = {uuid: crypto.randomUUID(), cmd: async () => await this.copyFilePromisify(filepath, target)};
|
2023-03-03 15:23:46 +00:00
|
|
|
await this.asyncQueue.waitFor(command);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public createDir(filepath: string): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-11-13 11:14:58 -05:00
|
|
|
fs.mkdirSync(filepath.substr(0, filepath.lastIndexOf("/")), {recursive: true});
|
2023-03-03 15:23:46 +00:00
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async createDirAsync(filepath: string): Promise<void>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const command = {
|
2023-11-07 23:27:52 -05:00
|
|
|
uuid: crypto.randomUUID(),
|
2023-11-13 11:14:58 -05:00
|
|
|
cmd: async () =>
|
|
|
|
await this.mkdirPromisify(filepath.substr(0, filepath.lastIndexOf("/")), {recursive: true}),
|
2023-03-03 15:23:46 +00:00
|
|
|
};
|
|
|
|
await this.asyncQueue.waitFor(command);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public copyDir(filepath: string, target: string, fileExtensions: string | string[] = undefined): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const files = this.getFiles(filepath);
|
|
|
|
const dirs = this.getDirs(filepath);
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
if (!this.exists(target))
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.createDir(`${target}/`);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const dir of dirs)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.copyDir(path.join(filepath, dir), path.join(target, dir), fileExtensions);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const file of files)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
// copy all if fileExtension is not set, copy only those with fileExtension if set
|
2023-11-07 23:29:25 -05:00
|
|
|
if (!fileExtensions || fileExtensions.includes(file.split(".").pop()))
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.copyFile(path.join(filepath, file), path.join(target, file));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async copyDirAsync(filepath: string, target: string, fileExtensions: string | string[]): Promise<void>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const files = this.getFiles(filepath);
|
|
|
|
const dirs = this.getDirs(filepath);
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
if (!await this.existsAsync(target))
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
await this.createDirAsync(`${target}/`);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const dir of dirs)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
await this.copyDirAsync(path.join(filepath, dir), path.join(target, dir), fileExtensions);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const file of files)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
// copy all if fileExtension is not set, copy only those with fileExtension if set
|
2023-11-07 23:29:25 -05:00
|
|
|
if (!fileExtensions || fileExtensions.includes(file.split(".").pop()))
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
await this.copyAsync(path.join(filepath, file), path.join(target, file));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public readFile(...args: Parameters<typeof fs.readFileSync>): string
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const read = fs.readFileSync(...args);
|
|
|
|
if (this.isBuffer(read))
|
2023-11-13 11:14:58 -05:00
|
|
|
{
|
2023-03-03 15:23:46 +00:00
|
|
|
return read.toString();
|
2023-11-13 11:14:58 -05:00
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
return read;
|
|
|
|
}
|
2023-11-07 23:29:25 -05:00
|
|
|
|
|
|
|
public async readFileAsync(path: fs.PathLike): Promise<string>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const read = await this.readFilePromisify(path);
|
|
|
|
if (this.isBuffer(read))
|
2023-11-13 11:14:58 -05:00
|
|
|
{
|
2023-03-03 15:23:46 +00:00
|
|
|
return read.toString();
|
2023-11-13 11:14:58 -05:00
|
|
|
}
|
2023-03-03 15:23:46 +00:00
|
|
|
return read;
|
|
|
|
}
|
|
|
|
|
|
|
|
private isBuffer(value: any): value is Buffer
|
|
|
|
{
|
|
|
|
return value?.write && value.toString && value.toJSON && value.equals;
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public writeFile(filepath: any, data = "", append = false, atomic = true): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-11-13 11:14:58 -05:00
|
|
|
const options = append ? {flag: "a"} : {flag: "w"};
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
if (!this.exists(filepath))
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.createDir(filepath);
|
|
|
|
fs.writeFileSync(filepath, "");
|
|
|
|
}
|
|
|
|
|
|
|
|
this.lockFileSync(filepath);
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
if (!append && atomic)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
writeFileSync(filepath, data);
|
|
|
|
}
|
2023-11-07 23:29:25 -05:00
|
|
|
else
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
fs.writeFileSync(filepath, data, options);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
if (this.checkFileSync(filepath))
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.unlockFileSync(filepath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async writeFileAsync(filepath: any, data = "", append = false, atomic = true): Promise<void>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-11-13 11:14:58 -05:00
|
|
|
const options = append ? {flag: "a"} : {flag: "w"};
|
2023-03-03 15:23:46 +00:00
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
if (!await this.exists(filepath))
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
await this.createDir(filepath);
|
|
|
|
await this.writeFilePromisify(filepath, "");
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
if (!append && atomic)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
await this.writeFilePromisify(filepath, data);
|
|
|
|
}
|
2023-11-07 23:29:25 -05:00
|
|
|
else
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
await this.writeFilePromisify(filepath, data, options);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public getFiles(filepath: string): string[]
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-11-07 23:29:25 -05:00
|
|
|
return fs.readdirSync(filepath).filter((item) =>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
return fs.statSync(path.join(filepath, item)).isFile();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async getFilesAsync(filepath: string): Promise<string[]>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const addr = await this.readdirPromisify(filepath);
|
2023-11-07 23:29:25 -05:00
|
|
|
return addr.filter(async (item) =>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const stat = await this.statPromisify(path.join(filepath, item));
|
|
|
|
return stat.isFile();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public getDirs(filepath: string): string[]
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
2023-11-07 23:29:25 -05:00
|
|
|
return fs.readdirSync(filepath).filter((item) =>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
return fs.statSync(path.join(filepath, item)).isDirectory();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async getDirsAsync(filepath: string): Promise<string[]>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const addr = await this.readdirPromisify(filepath);
|
2023-11-07 23:29:25 -05:00
|
|
|
return addr.filter(async (item) =>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const stat = await this.statPromisify(path.join(filepath, item));
|
|
|
|
return stat.isDirectory();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public removeFile(filepath: string): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
fs.unlinkSync(filepath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async removeFileAsync(filepath: string): Promise<void>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
await this.unlinkPromisify(filepath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public removeDir(filepath: string): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const files = this.getFiles(filepath);
|
|
|
|
const dirs = this.getDirs(filepath);
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const dir of dirs)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.removeDir(path.join(filepath, dir));
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const file of files)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.removeFile(path.join(filepath, file));
|
|
|
|
}
|
|
|
|
|
|
|
|
fs.rmdirSync(filepath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async removeDirAsync(filepath: string): Promise<void>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const files = this.getFiles(filepath);
|
|
|
|
const dirs = this.getDirs(filepath);
|
|
|
|
|
|
|
|
const promises = [];
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const dir of dirs)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
promises.push(this.removeDirAsync(path.join(filepath, dir)));
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const file of files)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
promises.push(this.removeFile(path.join(filepath, file)));
|
|
|
|
}
|
|
|
|
|
|
|
|
await Promise.all(promises);
|
|
|
|
await this.rmdirPromisify(filepath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public rename(oldPath: string, newPath: string): void
|
2023-10-10 11:03:20 +00:00
|
|
|
{
|
|
|
|
fs.renameSync(oldPath, newPath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async renameAsync(oldPath: string, newPath: string): Promise<void>
|
2023-10-10 11:03:20 +00:00
|
|
|
{
|
|
|
|
await this.renamePromisify(oldPath, newPath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
protected lockFileSync(filepath: any): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
lockfile.lockSync(filepath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
protected checkFileSync(filepath: any): any
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
return lockfile.checkSync(filepath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
protected unlockFileSync(filepath: any): void
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
lockfile.unlockSync(filepath);
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public getFileExtension(filepath: string): string
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
return filepath.split(".").pop();
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public stripExtension(filepath: string): string
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
return filepath.split(".").slice(0, -1).join(".");
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async minifyAllJsonInDirRecursive(filepath: string): Promise<void>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const files = this.getFiles(filepath).filter((item) => this.getFileExtension(item) === "json");
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const file of files)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const filePathAndName = path.join(filepath, file);
|
|
|
|
const minified = JSON.stringify(JSON.parse(this.readFile(filePathAndName)));
|
|
|
|
this.writeFile(filePathAndName, minified);
|
|
|
|
}
|
|
|
|
|
|
|
|
const dirs = this.getDirs(filepath);
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const dir of dirs)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
this.minifyAllJsonInDirRecursive(path.join(filepath, dir));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-07 23:29:25 -05:00
|
|
|
public async minifyAllJsonInDirRecursiveAsync(filepath: string): Promise<void>
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const files = this.getFiles(filepath).filter((item) => this.getFileExtension(item) === "json");
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const file of files)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
const filePathAndName = path.join(filepath, file);
|
|
|
|
const minified = JSON.stringify(JSON.parse(await this.readFile(filePathAndName)));
|
|
|
|
await this.writeFile(filePathAndName, minified);
|
|
|
|
}
|
|
|
|
|
|
|
|
const dirs = this.getDirs(filepath);
|
|
|
|
const promises: Promise<void>[] = [];
|
2023-11-07 23:29:25 -05:00
|
|
|
for (const dir of dirs)
|
2023-03-03 15:23:46 +00:00
|
|
|
{
|
|
|
|
promises.push(this.minifyAllJsonInDirRecursive(path.join(filepath, dir)));
|
|
|
|
}
|
|
|
|
await Promise.all(promises);
|
|
|
|
}
|
|
|
|
|
|
|
|
public getFilesOfType(directory: string, fileType: string, files: string[] = []): string[]
|
|
|
|
{
|
|
|
|
// no dir so exit early
|
|
|
|
if (!fs.existsSync(directory))
|
|
|
|
{
|
|
|
|
return files;
|
|
|
|
}
|
|
|
|
|
2023-11-13 11:14:58 -05:00
|
|
|
const dirents = fs.readdirSync(directory, {encoding: "utf-8", withFileTypes: true});
|
2023-03-03 15:23:46 +00:00
|
|
|
for (const dirent of dirents)
|
|
|
|
{
|
|
|
|
const res = resolve(directory, dirent.name);
|
|
|
|
if (dirent.isDirectory())
|
|
|
|
{
|
|
|
|
this.getFilesOfType(res, fileType, files);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
if (res.endsWith(fileType))
|
|
|
|
{
|
|
|
|
files.push(res);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return files;
|
|
|
|
}
|
|
|
|
}
|