feat: init vlc-sender
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
dist
|
||||
.git
|
||||
7
.env.example
Normal file
7
.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
# Тома для отображения в интерфейсе
|
||||
# Формат: label:/path,label:/path или просто /path1,/path2
|
||||
# Примеры:
|
||||
# MEDIA_PATHS=Torrents:/data/torrents,Media:/data/media
|
||||
# MEDIA_PATHS=/data/torrents,/data/media
|
||||
# MEDIA_PATHS=/media
|
||||
MEDIA_PATHS=Torrents:/data/torrents,Media:/data/media
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
19
Dockerfile
Normal file
19
Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN yarn build
|
||||
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile --production
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY src/public ./dist/public
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/index.js"]
|
||||
13
compose.yaml
Normal file
13
compose.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
vlc-sender:
|
||||
build: .
|
||||
container_name: vlc-sender
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env
|
||||
volumes:
|
||||
- /mnt/zfs-data/torrents:/data/torrents:ro
|
||||
- /mnt/zfs-data/media:/data/media:ro
|
||||
environment:
|
||||
- PORT=3000
|
||||
network_mode: host
|
||||
64
docs/vlc-sender.md
Normal file
64
docs/vlc-sender.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# VLC Sender
|
||||
|
||||
Веб-приложение для отправки медиафайлов на VLC через WiFi Upload API.
|
||||
|
||||
## Архитектура
|
||||
|
||||
- **Backend**: Express (TypeScript) — API для навигации по файлам и отправки на VLC
|
||||
- **Frontend**: Single-file HTML/CSS/JS в `src/public/index.html`
|
||||
- **Docker**: multi-stage build на node:22-alpine
|
||||
|
||||
## API
|
||||
|
||||
| Endpoint | Метод | Описание |
|
||||
|----------|-------|----------|
|
||||
| `/api/files?path=` | GET | Список файлов/папок (name, size, isDir). Корень — список томов |
|
||||
| `/api/send` | POST | Отправка файлов на VLC, возвращает SSE с прогрессом |
|
||||
|
||||
### POST /api/send
|
||||
```json
|
||||
{ "ip": "243", "files": ["Torrents/movie.mkv"] }
|
||||
```
|
||||
Путь файла: `{volume_label}/{subpath}`. Ответ — SSE stream с событиями: `start`, `done`, `error`, `complete`.
|
||||
|
||||
## VLC API
|
||||
|
||||
VLC принимает файлы через `POST http://192.168.1.{ip}:80/upload.json` (multipart/form-data, поле `file`).
|
||||
|
||||
## Переменные окружения
|
||||
|
||||
| Переменная | Значение по умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `PORT` | `3000` | Порт сервера |
|
||||
| `MEDIA_PATHS` | `/media` | Тома для отображения (см. ниже) |
|
||||
|
||||
### MEDIA_PATHS
|
||||
|
||||
Поддерживает несколько томов через запятую. Два формата:
|
||||
|
||||
```bash
|
||||
# С лейблами
|
||||
MEDIA_PATHS=Torrents:/data/torrents,Media:/data/media
|
||||
|
||||
# Без лейблов (имя директории = лейбл)
|
||||
MEDIA_PATHS=/data/torrents,/data/media
|
||||
```
|
||||
|
||||
Обратная совместимость: `MEDIA_PATH` (без S) тоже поддерживается для одного тома.
|
||||
|
||||
## Docker
|
||||
|
||||
```bash
|
||||
docker build -t vlc-sender .
|
||||
```
|
||||
|
||||
`compose.yaml` использует `network_mode: host` для доступа к устройствам в локальной сети. Тома настраиваются в `.env` (см. `.env.example`).
|
||||
|
||||
## Структура
|
||||
|
||||
```
|
||||
src/
|
||||
├── index.ts # Express сервер + API
|
||||
└── public/
|
||||
└── index.html # Фронтенд (single file)
|
||||
```
|
||||
20
package.json
Normal file
20
package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "vlc-sender",
|
||||
"version": "1.0.0",
|
||||
"description": "Web UI for sending media files to VLC via WiFi Upload API",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc && cp -r src/public dist/public",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "ts-node src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.2",
|
||||
"form-data": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.13.1",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
277
src/index.ts
Normal file
277
src/index.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import express, { Request, Response } from 'express';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import http from 'http';
|
||||
import FormData from 'form-data';
|
||||
|
||||
const app = express();
|
||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
||||
const SUBNET_PREFIX = '192.168.1.';
|
||||
|
||||
// Parse volumes: "label1:/path1,label2:/path2" or just "/path1,/path2"
|
||||
interface Volume {
|
||||
label: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
function parseVolumes(): Volume[] {
|
||||
const raw = process.env.MEDIA_PATHS || process.env.MEDIA_PATH || '/media';
|
||||
return raw
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean)
|
||||
.map((entry) => {
|
||||
const colonIdx = entry.indexOf(':');
|
||||
// If has label (e.g. "Torrents:/data/torrents")
|
||||
// But skip Windows-like paths or bare /paths
|
||||
if (colonIdx > 0 && !entry.startsWith('/')) {
|
||||
return {
|
||||
label: entry.substring(0, colonIdx),
|
||||
path: entry.substring(colonIdx + 1),
|
||||
};
|
||||
}
|
||||
// Just a path — use directory name as label
|
||||
return {
|
||||
label: path.basename(entry) || entry,
|
||||
path: entry,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const VOLUMES = parseVolumes();
|
||||
|
||||
function findVolume(volumeLabel: string): Volume | undefined {
|
||||
return VOLUMES.find((v) => v.label === volumeLabel);
|
||||
}
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
interface FileEntry {
|
||||
name: string;
|
||||
size: number;
|
||||
isDir: boolean;
|
||||
}
|
||||
|
||||
app.get('/api/files', (req: Request, res: Response): void => {
|
||||
const relativePath = (req.query.path as string) || '';
|
||||
|
||||
// Root level — list volumes
|
||||
if (!relativePath) {
|
||||
const volumes: FileEntry[] = VOLUMES.map((v) => ({
|
||||
name: v.label,
|
||||
size: 0,
|
||||
isDir: true,
|
||||
}));
|
||||
res.json(volumes);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse: first segment is volume label, rest is subpath
|
||||
const parts = relativePath.split('/');
|
||||
const volumeLabel = parts[0];
|
||||
const subPath = parts.slice(1).join('/');
|
||||
const volume = findVolume(volumeLabel);
|
||||
|
||||
if (!volume) {
|
||||
res.status(404).json({ error: `Volume not found: ${volumeLabel}` });
|
||||
return;
|
||||
}
|
||||
|
||||
const fullPath = path.join(volume.path, subPath);
|
||||
const resolved = path.resolve(fullPath);
|
||||
|
||||
if (!resolved.startsWith(path.resolve(volume.path))) {
|
||||
res.status(403).json({ error: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(resolved)) {
|
||||
console.error(`[files] Path does not exist: ${resolved}`);
|
||||
res.status(404).json({ error: `Directory not found: ${resolved}` });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
||||
const files: FileEntry[] = entries
|
||||
.filter((e) => !e.name.startsWith('.'))
|
||||
.map((e) => {
|
||||
const entryPath = path.join(resolved, e.name);
|
||||
try {
|
||||
const stat = fs.statSync(entryPath);
|
||||
return {
|
||||
name: e.name,
|
||||
size: stat.size,
|
||||
isDir: e.isDirectory(),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((e): e is FileEntry => e !== null)
|
||||
.sort((a, b) => {
|
||||
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
console.log(`[files] ${resolved} → ${files.length} entries`);
|
||||
res.json(files);
|
||||
} catch (err) {
|
||||
console.error(`[files] Error reading ${resolved}:`, err);
|
||||
res.status(500).json({ error: `Failed to read directory: ${String(err)}` });
|
||||
}
|
||||
});
|
||||
|
||||
interface SendRequest {
|
||||
ip: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
function resolveFilePath(filePath: string): string | null {
|
||||
const parts = filePath.split('/');
|
||||
const volumeLabel = parts[0];
|
||||
const subPath = parts.slice(1).join('/');
|
||||
const volume = findVolume(volumeLabel);
|
||||
if (!volume) return null;
|
||||
|
||||
const full = path.resolve(path.join(volume.path, subPath));
|
||||
if (!full.startsWith(path.resolve(volume.path))) return null;
|
||||
return full;
|
||||
}
|
||||
|
||||
function sendFileToVLC(
|
||||
filePath: string,
|
||||
vlcHost: string,
|
||||
): Promise<{ file: string; success: boolean; error?: string }> {
|
||||
return new Promise((resolve) => {
|
||||
const resolved = resolveFilePath(filePath);
|
||||
if (!resolved) {
|
||||
resolve({ file: filePath, success: false, error: 'Access denied' });
|
||||
return;
|
||||
}
|
||||
|
||||
let stat: fs.Stats;
|
||||
try {
|
||||
stat = fs.statSync(resolved);
|
||||
} catch {
|
||||
resolve({ file: filePath, success: false, error: 'File not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
const fileStream = fs.createReadStream(resolved);
|
||||
form.append('file', fileStream, {
|
||||
filename: path.basename(resolved),
|
||||
knownLength: stat.size,
|
||||
});
|
||||
|
||||
const reqOptions: http.RequestOptions = {
|
||||
hostname: vlcHost,
|
||||
port: 80,
|
||||
path: '/upload.json',
|
||||
method: 'POST',
|
||||
headers: form.getHeaders(),
|
||||
};
|
||||
|
||||
const req = http.request(reqOptions, (res) => {
|
||||
let body = '';
|
||||
res.on('data', (chunk: Buffer) => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
res.on('end', () => {
|
||||
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve({ file: filePath, success: true });
|
||||
} else {
|
||||
resolve({
|
||||
file: filePath,
|
||||
success: false,
|
||||
error: `VLC returned ${res.statusCode}: ${body}`,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err: Error) => {
|
||||
resolve({ file: filePath, success: false, error: err.message });
|
||||
});
|
||||
|
||||
form.pipe(req);
|
||||
});
|
||||
}
|
||||
|
||||
app.post('/api/send', (req: Request, res: Response): void => {
|
||||
const { ip, files } = req.body as SendRequest;
|
||||
|
||||
if (!ip || !files || !files.length) {
|
||||
res.status(400).json({ error: 'ip and files are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const ipNum = parseInt(ip, 10);
|
||||
if (isNaN(ipNum) || ipNum < 1 || ipNum > 254) {
|
||||
res.status(400).json({ error: 'Invalid IP suffix' });
|
||||
return;
|
||||
}
|
||||
|
||||
const vlcHost = `${SUBNET_PREFIX}${ip}`;
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive',
|
||||
});
|
||||
|
||||
const sendProgress = (data: Record<string, unknown>) => {
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
|
||||
const totalFiles = files.length;
|
||||
let completed = 0;
|
||||
|
||||
const processFiles = async () => {
|
||||
for (const file of files) {
|
||||
const fileName = path.basename(file);
|
||||
const resolved = resolveFilePath(file);
|
||||
|
||||
let fileSize = 0;
|
||||
if (resolved) {
|
||||
try {
|
||||
fileSize = fs.statSync(resolved).size;
|
||||
} catch {
|
||||
// will be caught by sendFileToVLC
|
||||
}
|
||||
}
|
||||
|
||||
sendProgress({
|
||||
type: 'start',
|
||||
file: fileName,
|
||||
index: completed,
|
||||
total: totalFiles,
|
||||
size: fileSize,
|
||||
});
|
||||
|
||||
const result = await sendFileToVLC(file, vlcHost);
|
||||
completed++;
|
||||
|
||||
sendProgress({
|
||||
type: result.success ? 'done' : 'error',
|
||||
file: fileName,
|
||||
index: completed - 1,
|
||||
total: totalFiles,
|
||||
error: result.error,
|
||||
});
|
||||
}
|
||||
|
||||
sendProgress({ type: 'complete', total: totalFiles });
|
||||
res.end();
|
||||
};
|
||||
|
||||
processFiles();
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`VLC Sender running on http://0.0.0.0:${PORT}`);
|
||||
console.log(`Volumes:`);
|
||||
VOLUMES.forEach((v) => console.log(` ${v.label} → ${v.path}`));
|
||||
});
|
||||
468
src/public/index.html
Normal file
468
src/public/index.html
Normal file
@@ -0,0 +1,468 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>VLC Sender</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #1a1a2e;
|
||||
color: #eee;
|
||||
min-height: 100vh;
|
||||
padding: 16px;
|
||||
}
|
||||
.container { max-width: 800px; margin: 0 auto; }
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 16px;
|
||||
color: #ff8c00;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
.ip-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
.ip-group span { color: #888; font-size: 0.9rem; }
|
||||
.ip-group input {
|
||||
width: 50px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #fff;
|
||||
font-size: 0.9rem;
|
||||
outline: none;
|
||||
text-align: center;
|
||||
}
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.btn:hover { opacity: 0.85; }
|
||||
.btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.btn-send { background: #ff8c00; color: #000; }
|
||||
.btn-back { background: #333; color: #eee; }
|
||||
.btn-select { background: #16213e; color: #eee; border: 1px solid #444; font-size: 0.8rem; padding: 6px 10px; }
|
||||
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.breadcrumb a {
|
||||
color: #ff8c00;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.breadcrumb a:hover { text-decoration: underline; }
|
||||
.breadcrumb span { color: #666; }
|
||||
|
||||
.file-list { list-style: none; }
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #222;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.file-item:hover { background: #16213e; }
|
||||
.file-item input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: #ff8c00;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.file-icon { font-size: 1.2rem; flex-shrink: 0; }
|
||||
.file-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.file-size { color: #888; font-size: 0.8rem; flex-shrink: 0; }
|
||||
|
||||
.progress-panel {
|
||||
margin-top: 16px;
|
||||
background: #16213e;
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
display: none;
|
||||
}
|
||||
.progress-panel.active { display: block; }
|
||||
.progress-item {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.progress-item:last-child { margin-bottom: 0; }
|
||||
.progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: #333;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
width: 0%;
|
||||
}
|
||||
.progress-fill.sending { background: #ff8c00; }
|
||||
.progress-fill.done { background: #4caf50; width: 100%; }
|
||||
.progress-fill.error { background: #f44336; width: 100%; }
|
||||
|
||||
.status-msg {
|
||||
margin-top: 8px;
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
}
|
||||
.status-msg.success { color: #4caf50; }
|
||||
.status-msg.error { color: #f44336; }
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.selected-count {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>VLC Sender</h1>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="ip-group">
|
||||
<span>192.168.1.</span>
|
||||
<input type="text" id="ipInput" placeholder="___" maxlength="3" inputmode="numeric">
|
||||
</div>
|
||||
<button class="btn btn-send" id="sendBtn" disabled>Отправить</button>
|
||||
<button class="btn btn-select" id="selectAllBtn">Выбрать все</button>
|
||||
<span class="selected-count" id="selectedCount"></span>
|
||||
</div>
|
||||
|
||||
<div class="breadcrumb" id="breadcrumb"></div>
|
||||
<ul class="file-list" id="fileList"></ul>
|
||||
|
||||
<div class="progress-panel" id="progressPanel"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
const fileList = document.getElementById('fileList');
|
||||
const breadcrumb = document.getElementById('breadcrumb');
|
||||
const ipInput = document.getElementById('ipInput');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
const selectAllBtn = document.getElementById('selectAllBtn');
|
||||
const selectedCount = document.getElementById('selectedCount');
|
||||
const progressPanel = document.getElementById('progressPanel');
|
||||
|
||||
let currentPath = '';
|
||||
let selectedFiles = new Set();
|
||||
let currentFiles = [];
|
||||
let sending = false;
|
||||
|
||||
// Restore last IP
|
||||
const savedIp = localStorage.getItem('vlc-ip');
|
||||
if (savedIp) ipInput.value = savedIp;
|
||||
|
||||
ipInput.addEventListener('input', () => {
|
||||
localStorage.setItem('vlc-ip', ipInput.value);
|
||||
updateSendBtn();
|
||||
});
|
||||
|
||||
function formatSize(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function getFileIcon(name, isDir) {
|
||||
if (isDir) return '\u{1F4C1}';
|
||||
const ext = name.split('.').pop().toLowerCase();
|
||||
const video = ['mkv', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'ts', 'm4v'];
|
||||
const audio = ['mp3', 'flac', 'aac', 'ogg', 'wav', 'wma', 'm4a'];
|
||||
const sub = ['srt', 'ass', 'ssa', 'sub', 'vtt'];
|
||||
if (video.includes(ext)) return '\u{1F3AC}';
|
||||
if (audio.includes(ext)) return '\u{1F3B5}';
|
||||
if (sub.includes(ext)) return '\u{1F4DD}';
|
||||
return '\u{1F4C4}';
|
||||
}
|
||||
|
||||
function updateSendBtn() {
|
||||
const ip = ipInput.value.trim();
|
||||
const ipNum = parseInt(ip, 10);
|
||||
const validIp = ip && !isNaN(ipNum) && ipNum >= 1 && ipNum <= 254;
|
||||
sendBtn.disabled = sending || selectedFiles.size === 0 || !validIp;
|
||||
}
|
||||
|
||||
function updateSelectedCount() {
|
||||
if (selectedFiles.size > 0) {
|
||||
selectedCount.textContent = 'Выбрано: ' + selectedFiles.size;
|
||||
} else {
|
||||
selectedCount.textContent = '';
|
||||
}
|
||||
updateSendBtn();
|
||||
}
|
||||
|
||||
function renderBreadcrumb() {
|
||||
const parts = currentPath ? currentPath.split('/') : [];
|
||||
let html = '<a data-path="">root</a>';
|
||||
let accumulated = '';
|
||||
for (const part of parts) {
|
||||
accumulated += (accumulated ? '/' : '') + part;
|
||||
html += '<span>/</span><a data-path="' + accumulated + '">' + part + '</a>';
|
||||
}
|
||||
breadcrumb.innerHTML = html;
|
||||
breadcrumb.querySelectorAll('a').forEach(a => {
|
||||
a.addEventListener('click', () => {
|
||||
navigateTo(a.dataset.path);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadFiles(dirPath) {
|
||||
try {
|
||||
const res = await fetch('/api/files?path=' + encodeURIComponent(dirPath));
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
throw new Error(data.error || 'HTTP ' + res.status);
|
||||
}
|
||||
return { files: await res.json(), error: null };
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return { files: [], error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function navigateTo(dirPath) {
|
||||
currentPath = dirPath;
|
||||
selectedFiles.clear();
|
||||
updateSelectedCount();
|
||||
renderBreadcrumb();
|
||||
|
||||
const { files, error } = await loadFiles(dirPath);
|
||||
currentFiles = files;
|
||||
if (error) {
|
||||
fileList.innerHTML = '<div class="empty" style="color:#f44336">Ошибка: ' + error + '</div>';
|
||||
} else {
|
||||
renderFiles(files);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFiles(files) {
|
||||
if (!files.length) {
|
||||
fileList.innerHTML = '<div class="empty">Пусто</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
fileList.innerHTML = '';
|
||||
for (const f of files) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'file-item';
|
||||
|
||||
const filePath = currentPath ? currentPath + '/' + f.name : f.name;
|
||||
|
||||
if (f.isDir) {
|
||||
li.innerHTML =
|
||||
'<span class="file-icon">' + getFileIcon(f.name, true) + '</span>' +
|
||||
'<span class="file-name">' + f.name + '</span>' +
|
||||
'<span class="file-size">' + formatSize(f.size) + '</span>';
|
||||
li.addEventListener('click', () => navigateTo(filePath));
|
||||
} else {
|
||||
const checked = selectedFiles.has(filePath) ? 'checked' : '';
|
||||
li.innerHTML =
|
||||
'<input type="checkbox" data-path="' + filePath + '" ' + checked + '>' +
|
||||
'<span class="file-icon">' + getFileIcon(f.name, false) + '</span>' +
|
||||
'<span class="file-name">' + f.name + '</span>' +
|
||||
'<span class="file-size">' + formatSize(f.size) + '</span>';
|
||||
|
||||
const cb = li.querySelector('input[type="checkbox"]');
|
||||
cb.addEventListener('change', (e) => {
|
||||
e.stopPropagation();
|
||||
if (cb.checked) {
|
||||
selectedFiles.add(filePath);
|
||||
} else {
|
||||
selectedFiles.delete(filePath);
|
||||
}
|
||||
updateSelectedCount();
|
||||
});
|
||||
|
||||
li.addEventListener('click', (e) => {
|
||||
if (e.target === cb) return;
|
||||
cb.checked = !cb.checked;
|
||||
cb.dispatchEvent(new Event('change'));
|
||||
});
|
||||
}
|
||||
|
||||
fileList.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
selectAllBtn.addEventListener('click', () => {
|
||||
const nonDirFiles = currentFiles.filter(f => !f.isDir);
|
||||
const allSelected = nonDirFiles.length > 0 && nonDirFiles.every(f => {
|
||||
const p = currentPath ? currentPath + '/' + f.name : f.name;
|
||||
return selectedFiles.has(p);
|
||||
});
|
||||
|
||||
if (allSelected) {
|
||||
// Deselect all
|
||||
for (const f of nonDirFiles) {
|
||||
const p = currentPath ? currentPath + '/' + f.name : f.name;
|
||||
selectedFiles.delete(p);
|
||||
}
|
||||
} else {
|
||||
// Select all
|
||||
for (const f of nonDirFiles) {
|
||||
const p = currentPath ? currentPath + '/' + f.name : f.name;
|
||||
selectedFiles.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
renderFiles(currentFiles);
|
||||
updateSelectedCount();
|
||||
});
|
||||
|
||||
sendBtn.addEventListener('click', () => {
|
||||
if (sending || selectedFiles.size === 0) return;
|
||||
const ip = ipInput.value.trim();
|
||||
if (!ip) return;
|
||||
|
||||
sending = true;
|
||||
updateSendBtn();
|
||||
sendBtn.textContent = 'Отправка...';
|
||||
|
||||
const files = Array.from(selectedFiles);
|
||||
progressPanel.classList.add('active');
|
||||
progressPanel.innerHTML = files.map((f, i) => {
|
||||
const name = f.split('/').pop();
|
||||
return '<div class="progress-item" id="prog-' + i + '">' +
|
||||
'<div class="progress-label"><span>' + name + '</span><span class="prog-status">Ожидание</span></div>' +
|
||||
'<div class="progress-bar"><div class="progress-fill" id="fill-' + i + '"></div></div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
const body = JSON.stringify({ ip, files });
|
||||
fetch('/api/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body,
|
||||
}).then(response => {
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
function read() {
|
||||
reader.read().then(({ done, value }) => {
|
||||
if (done) {
|
||||
sending = false;
|
||||
sendBtn.textContent = 'Отправить';
|
||||
updateSendBtn();
|
||||
return;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop();
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith('data: ')) continue;
|
||||
try {
|
||||
const data = JSON.parse(line.slice(6));
|
||||
handleProgress(data);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
read();
|
||||
});
|
||||
}
|
||||
read();
|
||||
}).catch(err => {
|
||||
sending = false;
|
||||
sendBtn.textContent = 'Отправить';
|
||||
updateSendBtn();
|
||||
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.className = 'status-msg error';
|
||||
statusDiv.textContent = 'Ошибка соединения: ' + err.message;
|
||||
progressPanel.appendChild(statusDiv);
|
||||
});
|
||||
});
|
||||
|
||||
function handleProgress(data) {
|
||||
const { type, file, index, total, error } = data;
|
||||
if (type === 'start') {
|
||||
const item = document.getElementById('prog-' + index);
|
||||
if (item) {
|
||||
item.querySelector('.prog-status').textContent = 'Отправка...';
|
||||
const fill = document.getElementById('fill-' + index);
|
||||
fill.className = 'progress-fill sending';
|
||||
fill.style.width = '50%';
|
||||
}
|
||||
} else if (type === 'done') {
|
||||
const item = document.getElementById('prog-' + index);
|
||||
if (item) {
|
||||
item.querySelector('.prog-status').textContent = 'Готово';
|
||||
const fill = document.getElementById('fill-' + index);
|
||||
fill.className = 'progress-fill done';
|
||||
}
|
||||
} else if (type === 'error') {
|
||||
const item = document.getElementById('prog-' + index);
|
||||
if (item) {
|
||||
item.querySelector('.prog-status').textContent = 'Ошибка';
|
||||
const fill = document.getElementById('fill-' + index);
|
||||
fill.className = 'progress-fill error';
|
||||
}
|
||||
const errDiv = document.createElement('div');
|
||||
errDiv.className = 'status-msg error';
|
||||
errDiv.textContent = file + ': ' + (error || 'Unknown error');
|
||||
progressPanel.appendChild(errDiv);
|
||||
} else if (type === 'complete') {
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.className = 'status-msg success';
|
||||
statusDiv.textContent = 'Отправка завершена (' + total + ' файлов)';
|
||||
progressPanel.appendChild(statusDiv);
|
||||
}
|
||||
}
|
||||
|
||||
// Initial load
|
||||
navigateTo('');
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "commonjs",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
633
yarn.lock
Normal file
633
yarn.lock
Normal file
@@ -0,0 +1,633 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.6.tgz#1859bebb8fd7dac9918a45d54c1971ab8b5af474"
|
||||
integrity sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==
|
||||
dependencies:
|
||||
"@types/connect" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect@*":
|
||||
version "3.4.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
|
||||
integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/express-serve-static-core@^5.0.0":
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz#1a77faffee9572d39124933259be2523837d7eaa"
|
||||
integrity sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express@^5.0.0":
|
||||
version "5.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.6.tgz#2d724b2c990dcb8c8444063f3580a903f6d500cc"
|
||||
integrity sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^5.0.0"
|
||||
"@types/serve-static" "^2"
|
||||
|
||||
"@types/http-errors@*":
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.5.tgz#5b749ab2b16ba113423feb1a64a95dcd30398472"
|
||||
integrity sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==
|
||||
|
||||
"@types/node@*":
|
||||
version "25.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-25.2.1.tgz#378021f9e765bb65ba36de16f3c3a8622c1fa03d"
|
||||
integrity sha512-CPrnr8voK8vC6eEtyRzvMpgp3VyVRhgclonE7qYi6P9sXwYb59ucfrnmFBTaP0yUi8Gk4yZg/LlTJULGxvTNsg==
|
||||
dependencies:
|
||||
undici-types "~7.16.0"
|
||||
|
||||
"@types/node@^22.13.1":
|
||||
version "22.19.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.19.9.tgz#2f7c7b09fe8441c4520e9a791ab615a313917111"
|
||||
integrity sha512-PD03/U8g1F9T9MI+1OBisaIARhSzeidsUjQaf51fOxrfjeiKN9bLVO06lHuHYjxdnqLWJijJHfqXPSJri2EM2A==
|
||||
dependencies:
|
||||
undici-types "~6.21.0"
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.14.0.tgz#d8b60cecf62f2db0fb68e5e006077b9178b85de5"
|
||||
integrity sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||
|
||||
"@types/send@*":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/send/-/send-1.2.1.tgz#6a784e45543c18c774c049bff6d3dbaf045c9c74"
|
||||
integrity sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/serve-static@^2":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-2.2.0.tgz#d4a447503ead0d1671132d1ab6bd58b805d8de6a"
|
||||
integrity sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==
|
||||
dependencies:
|
||||
"@types/http-errors" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
accepts@~1.3.8:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
||||
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
|
||||
dependencies:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
array-flatten@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
|
||||
|
||||
body-parser@~1.20.3:
|
||||
version "1.20.4"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.4.tgz#f8e20f4d06ca8a50a71ed329c15dccad1cdc547f"
|
||||
integrity sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==
|
||||
dependencies:
|
||||
bytes "~3.1.2"
|
||||
content-type "~1.0.5"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "~1.2.0"
|
||||
http-errors "~2.0.1"
|
||||
iconv-lite "~0.4.24"
|
||||
on-finished "~2.4.1"
|
||||
qs "~6.14.0"
|
||||
raw-body "~2.5.3"
|
||||
type-is "~1.6.18"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
bytes@~3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||
|
||||
call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
|
||||
integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
function-bind "^1.1.2"
|
||||
|
||||
call-bound@^1.0.2:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a"
|
||||
integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
get-intrinsic "^1.3.0"
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
content-disposition@~0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
||||
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
|
||||
dependencies:
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
content-type@~1.0.4, content-type@~1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
|
||||
cookie-signature@~1.0.6:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454"
|
||||
integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
|
||||
|
||||
cookie@~0.7.1:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
||||
|
||||
debug@2.6.9:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
depd@2.0.0, depd@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||
|
||||
destroy@1.2.0, destroy@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||
|
||||
dunder-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
|
||||
integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
gopd "^1.2.0"
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||
|
||||
encodeurl@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
|
||||
es-define-property@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
|
||||
integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
|
||||
|
||||
es-errors@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
|
||||
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
|
||||
|
||||
es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
|
||||
integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
|
||||
es-set-tostringtag@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
|
||||
integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.6"
|
||||
has-tostringtag "^1.0.2"
|
||||
hasown "^2.0.2"
|
||||
|
||||
escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
||||
|
||||
etag@~1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
express@^4.21.2:
|
||||
version "4.22.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.22.1.tgz#1de23a09745a4fffdb39247b344bb5eaff382069"
|
||||
integrity sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "~1.20.3"
|
||||
content-disposition "~0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "~0.7.1"
|
||||
cookie-signature "~1.0.6"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
encodeurl "~2.0.0"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
finalhandler "~1.3.1"
|
||||
fresh "~0.5.2"
|
||||
http-errors "~2.0.0"
|
||||
merge-descriptors "1.0.3"
|
||||
methods "~1.1.2"
|
||||
on-finished "~2.4.1"
|
||||
parseurl "~1.3.3"
|
||||
path-to-regexp "~0.1.12"
|
||||
proxy-addr "~2.0.7"
|
||||
qs "~6.14.0"
|
||||
range-parser "~1.2.1"
|
||||
safe-buffer "5.2.1"
|
||||
send "~0.19.0"
|
||||
serve-static "~1.16.2"
|
||||
setprototypeof "1.2.0"
|
||||
statuses "~2.0.1"
|
||||
type-is "~1.6.18"
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
finalhandler@~1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88"
|
||||
integrity sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
encodeurl "~2.0.0"
|
||||
escape-html "~1.0.3"
|
||||
on-finished "~2.4.1"
|
||||
parseurl "~1.3.3"
|
||||
statuses "~2.0.2"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
form-data@^4.0.1:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053"
|
||||
integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
es-set-tostringtag "^2.1.0"
|
||||
hasown "^2.0.2"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
forwarded@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||
|
||||
fresh@~0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
||||
|
||||
function-bind@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
|
||||
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
|
||||
|
||||
get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
|
||||
integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
|
||||
dependencies:
|
||||
call-bind-apply-helpers "^1.0.2"
|
||||
es-define-property "^1.0.1"
|
||||
es-errors "^1.3.0"
|
||||
es-object-atoms "^1.1.1"
|
||||
function-bind "^1.1.2"
|
||||
get-proto "^1.0.1"
|
||||
gopd "^1.2.0"
|
||||
has-symbols "^1.1.0"
|
||||
hasown "^2.0.2"
|
||||
math-intrinsics "^1.1.0"
|
||||
|
||||
get-proto@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
|
||||
integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
|
||||
dependencies:
|
||||
dunder-proto "^1.0.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
gopd@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
|
||||
integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
|
||||
|
||||
has-symbols@^1.0.3, has-symbols@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
|
||||
integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
|
||||
|
||||
has-tostringtag@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
|
||||
integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
|
||||
dependencies:
|
||||
has-symbols "^1.0.3"
|
||||
|
||||
hasown@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
|
||||
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
|
||||
dependencies:
|
||||
function-bind "^1.1.2"
|
||||
|
||||
http-errors@~2.0.0, http-errors@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
|
||||
integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
|
||||
dependencies:
|
||||
depd "~2.0.0"
|
||||
inherits "~2.0.4"
|
||||
setprototypeof "~1.2.0"
|
||||
statuses "~2.0.2"
|
||||
toidentifier "~1.0.1"
|
||||
|
||||
iconv-lite@~0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
inherits@~2.0.4:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
|
||||
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||
|
||||
merge-descriptors@1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
|
||||
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
|
||||
|
||||
methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
mime@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==
|
||||
|
||||
ms@2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
negotiator@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||
|
||||
object-inspect@^1.13.3:
|
||||
version "1.13.4"
|
||||
resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213"
|
||||
integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==
|
||||
|
||||
on-finished@~2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||
dependencies:
|
||||
ee-first "1.1.1"
|
||||
|
||||
parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||
|
||||
path-to-regexp@~0.1.12:
|
||||
version "0.1.12"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.12.tgz#d5e1a12e478a976d432ef3c58d534b9923164bb7"
|
||||
integrity sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==
|
||||
|
||||
proxy-addr@~2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
|
||||
dependencies:
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
qs@~6.14.0:
|
||||
version "6.14.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
|
||||
integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
|
||||
dependencies:
|
||||
side-channel "^1.1.0"
|
||||
|
||||
range-parser@~1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@~2.5.3:
|
||||
version "2.5.3"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.3.tgz#11c6650ee770a7de1b494f197927de0c923822e2"
|
||||
integrity sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==
|
||||
dependencies:
|
||||
bytes "~3.1.2"
|
||||
http-errors "~2.0.1"
|
||||
iconv-lite "~0.4.24"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3":
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
send@~0.19.0, send@~0.19.1:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29"
|
||||
integrity sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
encodeurl "~2.0.0"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
fresh "~0.5.2"
|
||||
http-errors "~2.0.1"
|
||||
mime "1.6.0"
|
||||
ms "2.1.3"
|
||||
on-finished "~2.4.1"
|
||||
range-parser "~1.2.1"
|
||||
statuses "~2.0.2"
|
||||
|
||||
serve-static@~1.16.2:
|
||||
version "1.16.3"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.16.3.tgz#a97b74d955778583f3862a4f0b841eb4d5d78cf9"
|
||||
integrity sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==
|
||||
dependencies:
|
||||
encodeurl "~2.0.0"
|
||||
escape-html "~1.0.3"
|
||||
parseurl "~1.3.3"
|
||||
send "~0.19.1"
|
||||
|
||||
setprototypeof@1.2.0, setprototypeof@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
side-channel-list@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad"
|
||||
integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
object-inspect "^1.13.3"
|
||||
|
||||
side-channel-map@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42"
|
||||
integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==
|
||||
dependencies:
|
||||
call-bound "^1.0.2"
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.5"
|
||||
object-inspect "^1.13.3"
|
||||
|
||||
side-channel-weakmap@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea"
|
||||
integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==
|
||||
dependencies:
|
||||
call-bound "^1.0.2"
|
||||
es-errors "^1.3.0"
|
||||
get-intrinsic "^1.2.5"
|
||||
object-inspect "^1.13.3"
|
||||
side-channel-map "^1.0.1"
|
||||
|
||||
side-channel@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9"
|
||||
integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==
|
||||
dependencies:
|
||||
es-errors "^1.3.0"
|
||||
object-inspect "^1.13.3"
|
||||
side-channel-list "^1.0.0"
|
||||
side-channel-map "^1.0.1"
|
||||
side-channel-weakmap "^1.0.2"
|
||||
|
||||
statuses@~2.0.1, statuses@~2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
|
||||
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
|
||||
|
||||
toidentifier@~1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||
dependencies:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
typescript@^5.7.3:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||
|
||||
undici-types@~6.21.0:
|
||||
version "6.21.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb"
|
||||
integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==
|
||||
|
||||
undici-types@~7.16.0:
|
||||
version "7.16.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46"
|
||||
integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
|
||||
|
||||
unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||
|
||||
utils-merge@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
Reference in New Issue
Block a user