style: run prettier on entire codebase

This commit is contained in:
BOHA
2026-03-24 19:59:14 +01:00
parent 872be42107
commit 3c167cf5c4
148 changed files with 26740 additions and 13990 deletions

View File

@@ -1,67 +1,84 @@
import fs from 'fs';
import path from 'path';
import { config } from '../config/env';
import fs from "fs";
import path from "path";
import { config } from "../config/env";
const FileType = require('file-type') as typeof import('file-type');
const FileType = require("file-type") as typeof import("file-type");
const BLOCKED_EXTENSIONS = new Set([
'exe', 'bat', 'sh', 'php', 'htaccess', 'env', 'cmd', 'com', 'msi', 'ps1',
'vbs', 'vbe', 'js', 'ws', 'wsf', 'scr', 'pif', 'jar', 'reg',
"exe",
"bat",
"sh",
"php",
"htaccess",
"env",
"cmd",
"com",
"msi",
"ps1",
"vbs",
"vbe",
"js",
"ws",
"wsf",
"scr",
"pif",
"jar",
"reg",
]);
const SUSPICIOUS_MIMES = [
'application/x-executable',
'application/x-msdos-program',
'application/x-dosexec',
'application/x-msdownload',
"application/x-executable",
"application/x-msdos-program",
"application/x-dosexec",
"application/x-msdownload",
];
const MIME_MAP: Record<string, string> = {
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
zip: 'application/zip',
rar: 'application/x-rar-compressed',
'7z': 'application/x-7z-compressed',
tar: 'application/x-tar',
gz: 'application/gzip',
png: 'image/png',
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
gif: 'image/gif',
bmp: 'image/bmp',
svg: 'image/svg+xml',
webp: 'image/webp',
ico: 'image/x-icon',
tif: 'image/tiff',
tiff: 'image/tiff',
mp3: 'audio/mpeg',
wav: 'audio/wav',
mp4: 'video/mp4',
avi: 'video/x-msvideo',
mkv: 'video/x-matroska',
mov: 'video/quicktime',
txt: 'text/plain',
csv: 'text/csv',
html: 'text/html',
htm: 'text/html',
xml: 'application/xml',
json: 'application/json',
dwg: 'application/acad',
dxf: 'application/dxf',
step: 'application/step',
stp: 'application/step',
iges: 'application/iges',
igs: 'application/iges',
pdf: "application/pdf",
doc: "application/msword",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
xls: "application/vnd.ms-excel",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
ppt: "application/vnd.ms-powerpoint",
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
zip: "application/zip",
rar: "application/x-rar-compressed",
"7z": "application/x-7z-compressed",
tar: "application/x-tar",
gz: "application/gzip",
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
bmp: "image/bmp",
svg: "image/svg+xml",
webp: "image/webp",
ico: "image/x-icon",
tif: "image/tiff",
tiff: "image/tiff",
mp3: "audio/mpeg",
wav: "audio/wav",
mp4: "video/mp4",
avi: "video/x-msvideo",
mkv: "video/x-matroska",
mov: "video/quicktime",
txt: "text/plain",
csv: "text/csv",
html: "text/html",
htm: "text/html",
xml: "application/xml",
json: "application/json",
dwg: "application/acad",
dxf: "application/dxf",
step: "application/step",
stp: "application/step",
iges: "application/iges",
igs: "application/iges",
};
interface FileItem {
name: string;
type: 'file' | 'folder';
type: "file" | "folder";
modified: string;
is_symlink: boolean;
link_target?: string;
@@ -88,23 +105,28 @@ export class NasFileManager {
private readonly basePath: string;
constructor() {
this.basePath = path.resolve(config.nas.path).replace(/\\/g, '/');
this.basePath = path.resolve(config.nas.path).replace(/\\/g, "/");
}
public isConfigured(): boolean {
if (!this.basePath) return false;
try {
return fs.existsSync(this.basePath) && fs.statSync(this.basePath).isDirectory();
return (
fs.existsSync(this.basePath) && fs.statSync(this.basePath).isDirectory()
);
} catch {
return false;
}
}
public createProjectFolder(projectNumber: string, projectName: string): boolean {
public createProjectFolder(
projectNumber: string,
projectName: string,
): boolean {
if (!this.isConfigured()) return false;
const folderName = this.buildFolderName(projectNumber, projectName);
const fullPath = this.basePath + '/' + folderName;
const fullPath = this.basePath + "/" + folderName;
if (fs.existsSync(fullPath) && fs.statSync(fullPath).isDirectory()) {
return true;
@@ -143,7 +165,7 @@ export class NasFileManager {
if (currentPath === null) return false;
const newFolderName = this.buildFolderName(projectNumber, newName);
const newPath = this.basePath + '/' + newFolderName;
const newPath = this.basePath + "/" + newFolderName;
if (currentPath === newPath) return true;
@@ -155,7 +177,10 @@ export class NasFileManager {
}
}
public listFiles(projectNumber: string, subPath: string = ''): ListFilesResult | null {
public listFiles(
projectNumber: string,
subPath: string = "",
): ListFilesResult | null {
const dirPath = this.resolveProjectPath(projectNumber, subPath);
if (dirPath === null) return null;
@@ -175,7 +200,7 @@ export class NasFileManager {
const items: FileItem[] = [];
for (const entry of entries) {
const fullPath = dirPath + '/' + entry;
const fullPath = dirPath + "/" + entry;
let lstat: fs.Stats;
try {
@@ -200,14 +225,18 @@ export class NasFileManager {
const modified = lstat.mtime;
const modifiedStr =
modified.getFullYear() +
'-' + String(modified.getMonth() + 1).padStart(2, '0') +
'-' + String(modified.getDate()).padStart(2, '0') +
' ' + String(modified.getHours()).padStart(2, '0') +
':' + String(modified.getMinutes()).padStart(2, '0');
"-" +
String(modified.getMonth() + 1).padStart(2, "0") +
"-" +
String(modified.getDate()).padStart(2, "0") +
" " +
String(modified.getHours()).padStart(2, "0") +
":" +
String(modified.getMinutes()).padStart(2, "0");
const item: FileItem = {
name: entry,
type: isDir ? 'folder' : 'file',
type: isDir ? "folder" : "file",
modified: modifiedStr,
is_symlink: isLink,
};
@@ -215,7 +244,7 @@ export class NasFileManager {
if (isLink) {
try {
const target = fs.readlinkSync(fullPath);
item.link_target = target.replace(/\//g, '\\');
item.link_target = target.replace(/\//g, "\\");
} catch {
// ignore
}
@@ -236,14 +265,20 @@ export class NasFileManager {
// Sort: folders first, then files, both alphabetically (natural sort)
items.sort((a, b) => {
if (a.type !== b.type) {
return a.type === 'folder' ? -1 : 1;
return a.type === "folder" ? -1 : 1;
}
return a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' });
return a.name.localeCompare(b.name, undefined, {
numeric: true,
sensitivity: "base",
});
});
const breadcrumb: string[] = [''];
if (subPath !== '') {
const parts = subPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '').split('/');
const breadcrumb: string[] = [""];
if (subPath !== "") {
const parts = subPath
.replace(/\\/g, "/")
.replace(/^\/+|\/+$/g, "")
.split("/");
for (const part of parts) {
breadcrumb.push(part);
}
@@ -261,7 +296,7 @@ export class NasFileManager {
path: subPath,
items,
breadcrumb,
full_path: realDirPath.replace(/\//g, '\\'),
full_path: realDirPath.replace(/\//g, "\\"),
};
}
@@ -272,8 +307,12 @@ export class NasFileManager {
fileName: string,
): Promise<string | null> {
const dirPath = this.resolveProjectPath(projectNumber, subPath);
if (dirPath === null || !fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
return 'Cílová složka neexistuje';
if (
dirPath === null ||
!fs.existsSync(dirPath) ||
!fs.statSync(dirPath).isDirectory()
) {
return "Cílová složka neexistuje";
}
if (fileBuffer.length > config.nas.maxUploadSize) {
@@ -283,34 +322,34 @@ export class NasFileManager {
const originalName = path.basename(fileName);
let safeName = this.sanitizeFilename(originalName);
if (safeName === '') {
return 'Neplatný název souboru';
if (safeName === "") {
return "Neplatný název souboru";
}
const ext = path.extname(safeName).slice(1).toLowerCase();
if (BLOCKED_EXTENSIONS.has(ext)) {
return 'Tento typ souboru není povolen';
return "Tento typ souboru není povolen";
}
// MIME validation via file-type
try {
const typeResult = await FileType.fromBuffer(fileBuffer);
if (typeResult && this.isSuspiciousMime(typeResult.mime, ext)) {
return 'Obsah souboru neodpovídá jeho příponě';
return "Obsah souboru neodpovídá jeho příponě";
}
} catch {
// If file-type fails, continue without MIME check
}
let destPath = dirPath + '/' + safeName;
let destPath = dirPath + "/" + safeName;
// If file exists, append counter
if (fs.existsSync(destPath)) {
const base = path.basename(safeName, ext ? '.' + ext : '');
const base = path.basename(safeName, ext ? "." + ext : "");
let counter = 1;
do {
safeName = base + '_' + counter + (ext ? '.' + ext : '');
destPath = dirPath + '/' + safeName;
safeName = base + "_" + counter + (ext ? "." + ext : "");
destPath = dirPath + "/" + safeName;
counter++;
} while (fs.existsSync(destPath));
}
@@ -318,7 +357,7 @@ export class NasFileManager {
try {
fs.writeFileSync(destPath, fileBuffer);
} catch {
return 'Nepodařilo se uložit soubor';
return "Nepodařilo se uložit soubor";
}
return null;
@@ -340,7 +379,7 @@ export class NasFileManager {
const fileName = path.basename(fullPath);
const ext = path.extname(fileName).slice(1).toLowerCase();
const mime = MIME_MAP[ext] || 'application/octet-stream';
const mime = MIME_MAP[ext] || "application/octet-stream";
return {
filePath: fullPath,
@@ -349,25 +388,28 @@ export class NasFileManager {
};
}
public async deleteItem(projectNumber: string, filePath: string): Promise<string | null> {
if (filePath === '' || filePath === '/') {
return 'Nelze smazat kořenovou složku projektu';
public async deleteItem(
projectNumber: string,
filePath: string,
): Promise<string | null> {
if (filePath === "" || filePath === "/") {
return "Nelze smazat kořenovou složku projektu";
}
const fullPath = this.resolveProjectPath(projectNumber, filePath);
if (fullPath === null) {
return 'Neplatná cesta';
return "Neplatná cesta";
}
if (!fs.existsSync(fullPath)) {
return 'Soubor nebo složka neexistuje';
return "Soubor nebo složka neexistuje";
}
let isDir: boolean;
try {
isDir = fs.lstatSync(fullPath).isDirectory();
} catch {
return 'Neplatná cesta';
return "Neplatná cesta";
}
try {
@@ -378,76 +420,92 @@ export class NasFileManager {
}
} catch {
return isDir
? 'Nepodařilo se smazat složku'
: 'Nepodařilo se smazat soubor';
? "Nepodařilo se smazat složku"
: "Nepodařilo se smazat soubor";
}
return null;
}
public moveItem(projectNumber: string, fromPath: string, toPath: string): string | null {
if (fromPath === '' || fromPath === '/') {
return 'Nelze přesunout kořenovou složku';
public moveItem(
projectNumber: string,
fromPath: string,
toPath: string,
): string | null {
if (fromPath === "" || fromPath === "/") {
return "Nelze přesunout kořenovou složku";
}
const fullFrom = this.resolveProjectPath(projectNumber, fromPath);
const fullTo = this.resolveProjectPath(projectNumber, toPath);
if (fullFrom === null || fullTo === null) {
return 'Neplatná cesta';
return "Neplatná cesta";
}
if (!fs.existsSync(fullFrom)) {
return 'Zdrojový soubor neexistuje';
return "Zdrojový soubor neexistuje";
}
// Case-insensitive FS (Windows) — allow case-only rename
const sameFile =
fullFrom.replace(/\\/g, '/').toLowerCase() ===
fullTo.replace(/\\/g, '/').toLowerCase();
fullFrom.replace(/\\/g, "/").toLowerCase() ===
fullTo.replace(/\\/g, "/").toLowerCase();
if (fs.existsSync(fullTo) && !sameFile) {
return 'Cílový soubor již existuje';
return "Cílový soubor již existuje";
}
// Validate target name
const targetName = path.basename(toPath);
if (this.sanitizeFilename(targetName) !== targetName) {
return 'Neplatný cílový název';
return "Neplatný cílový název";
}
try {
fs.renameSync(fullFrom, fullTo);
} catch (err: unknown) {
if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code === 'EXDEV') {
return 'Přesun mezi různými disky není podporován';
if (
err instanceof Error &&
"code" in err &&
(err as NodeJS.ErrnoException).code === "EXDEV"
) {
return "Přesun mezi různými disky není podporován";
}
return 'Nepodařilo se přesunout soubor';
return "Nepodařilo se přesunout soubor";
}
return null;
}
public createFolder(projectNumber: string, subPath: string, folderName: string): string | null {
public createFolder(
projectNumber: string,
subPath: string,
folderName: string,
): string | null {
const dirPath = this.resolveProjectPath(projectNumber, subPath);
if (dirPath === null || !fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
return 'Nadřazená složka neexistuje';
if (
dirPath === null ||
!fs.existsSync(dirPath) ||
!fs.statSync(dirPath).isDirectory()
) {
return "Nadřazená složka neexistuje";
}
const safeName = this.sanitizeFilename(folderName);
if (safeName === '') {
return 'Neplatný název složky';
if (safeName === "") {
return "Neplatný název složky";
}
const newPath = dirPath + '/' + safeName;
const newPath = dirPath + "/" + safeName;
if (fs.existsSync(newPath)) {
return 'Složka s tímto názvem již existuje';
return "Složka s tímto názvem již existuje";
}
try {
fs.mkdirSync(newPath, { mode: 0o775 });
} catch {
return 'Nepodařilo se vytvořit složku';
return "Nepodařilo se vytvořit složku";
}
return null;
@@ -456,15 +514,15 @@ export class NasFileManager {
public sanitizeFilename(name: string): string {
let safe = path.basename(name);
// Strip control chars and special chars
safe = safe.replace(/[\x00-\x1f\x7f<>:"/\\|?*]/g, '');
safe = safe.replace(/^[. ]+|[. ]+$/g, '');
safe = safe.replace(/[\x00-\x1f\x7f<>:"/\\|?*]/g, "");
safe = safe.replace(/^[. ]+|[. ]+$/g, "");
if ([...safe].length > 255) {
const ext = path.extname(safe).slice(1);
const base = path.basename(safe, ext ? '.' + ext : '');
const base = path.basename(safe, ext ? "." + ext : "");
const maxBase = 250 - [...ext].length;
const trimmedBase = [...base].slice(0, maxBase).join('');
safe = ext ? trimmedBase + '.' + ext : trimmedBase;
const trimmedBase = [...base].slice(0, maxBase).join("");
safe = ext ? trimmedBase + "." + ext : trimmedBase;
}
return safe;
@@ -482,10 +540,10 @@ export class NasFileManager {
return null;
}
const prefix = projectNumber + '_';
const prefix = projectNumber + "_";
for (const entry of entries) {
if (entry.startsWith(prefix)) {
const fullPath = this.basePath + '/' + entry;
const fullPath = this.basePath + "/" + entry;
try {
if (fs.statSync(fullPath).isDirectory()) {
return fullPath;
@@ -500,35 +558,41 @@ export class NasFileManager {
}
private buildFolderName(projectNumber: string, projectName: string): string {
let safe = projectName.replace(/[^\p{L}\p{N}_\-. ]/gu, '');
safe = safe.trim().replace(/ /g, '_');
safe = safe.replace(/_+/g, '_');
let safe = projectName.replace(/[^\p{L}\p{N}_\-. ]/gu, "");
safe = safe.trim().replace(/ /g, "_");
safe = safe.replace(/_+/g, "_");
if ([...safe].length > 200) {
safe = [...safe].slice(0, 200).join('');
safe = [...safe].slice(0, 200).join("");
}
return projectNumber + '_' + safe;
return projectNumber + "_" + safe;
}
private resolveProjectPath(projectNumber: string, subPath: string): string | null {
private resolveProjectPath(
projectNumber: string,
subPath: string,
): string | null {
const folderPath = this.findProjectFolder(projectNumber);
if (folderPath === null) return null;
if (subPath === '' || subPath === '/') {
if (subPath === "" || subPath === "/") {
return folderPath;
}
// Basic path traversal protection
if (subPath.includes('\0') || subPath.includes('..')) {
if (subPath.includes("\0") || subPath.includes("..")) {
return null;
}
// Normalize separators and trim
const normalized = subPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
const candidate = path.resolve(folderPath, normalized).replace(/\\/g, '/');
const normalized = subPath.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
const candidate = path.resolve(folderPath, normalized).replace(/\\/g, "/");
// Verify candidate is within project folder
const normalFolder = folderPath.replace(/\\/g, '/');
if (!candidate.startsWith(normalFolder + '/') && candidate !== normalFolder) {
const normalFolder = folderPath.replace(/\\/g, "/");
if (
!candidate.startsWith(normalFolder + "/") &&
candidate !== normalFolder
) {
return null;
}
@@ -551,8 +615,8 @@ export class NasFileManager {
}
private walkAndRejectSymlinks(fullPath: string, basePath: string): boolean {
const normalFull = fullPath.replace(/\\/g, '/');
const normalBase = basePath.replace(/\\/g, '/');
const normalFull = fullPath.replace(/\\/g, "/");
const normalBase = basePath.replace(/\\/g, "/");
// Get the relative portion after basePath
if (!normalFull.startsWith(normalBase)) {
@@ -562,11 +626,11 @@ export class NasFileManager {
const relative = normalFull.slice(normalBase.length);
if (!relative) return true; // same as base
const parts = relative.split('/').filter(Boolean);
const parts = relative.split("/").filter(Boolean);
let current = normalBase;
for (const part of parts) {
current = current + '/' + part;
current = current + "/" + part;
try {
const lstat = fs.lstatSync(current);
if (lstat.isSymbolicLink()) {
@@ -592,15 +656,15 @@ export class NasFileManager {
private formatFileSize(bytes: number): string {
if (bytes < 1024) {
return bytes + ' B';
return bytes + " B";
}
if (bytes < 1048576) {
return (Math.round((bytes / 1024) * 10) / 10) + ' KB';
return Math.round((bytes / 1024) * 10) / 10 + " KB";
}
if (bytes < 1073741824) {
return (Math.round((bytes / 1048576) * 10) / 10) + ' MB';
return Math.round((bytes / 1048576) * 10) / 10 + " MB";
}
return (Math.round((bytes / 1073741824) * 10) / 10) + ' GB';
return Math.round((bytes / 1073741824) * 10) / 10 + " GB";
}
private isSuspiciousMime(mime: string, ext: string): boolean {
@@ -609,7 +673,7 @@ export class NasFileManager {
}
// PHP files
if (mime.includes('php') || mime.includes('x-httpd')) {
if (mime.includes("php") || mime.includes("x-httpd")) {
return true;
}