Commit iniziale

This commit is contained in:
2026-03-05 14:45:06 +01:00
commit 7008f57119
13 changed files with 5162 additions and 0 deletions

40
.gitignore vendored Normal file
View File

@@ -0,0 +1,40 @@
# Dependencies
node_modules/
# Build output
dist/
out/
release/
*.exe
*.dmg
*.AppImage
*.deb
*.rpm
*.snap
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Environment files
.env
.env.*
!.env.example
# Temporary files
*.tmp
*.temp
# OS files
.DS_Store
Thumbs.db
# IDE/editor
.vscode/
.idea/
*.swp
*.swo

22
config/defaultConfig.json Normal file
View File

@@ -0,0 +1,22 @@
{
"rules": [
{
"ext": "prt",
"pattern": "^1",
"destination": "./output/clienti1/PRT"
},
{
"ext": "prt",
"pattern": "^6",
"destination": "./output/clienti2/PRT"
},
{
"ext": "asm",
"destination": "./output/assembly"
},
{
"ext": "drw",
"destination": "./output/drawing"
}
]
}

72
main.js Normal file
View File

@@ -0,0 +1,72 @@
const { app, BrowserWindow, dialog, ipcMain } = require('electron');
const path = require('path');
const { processFolder } = require('./services/folderProcessor');
const { processZip } = require('./services/zipProcessor');
const { loadConfig } = require('./services/configService');
let config;
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 640,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
}
async function initConfig() {
config = await loadConfig();
}
app.whenReady().then(async () => {
await initConfig();
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
ipcMain.handle('select-folder', async () => {
const result = await dialog.showOpenDialog({
properties: ['openDirectory'],
});
if (result.canceled || !result.filePaths[0]) {
return { canceled: true };
}
const routingResult = await processFolder(result.filePaths[0], config);
return { canceled: false, ...routingResult };
});
ipcMain.handle('select-zip', async () => {
const result = await dialog.showOpenDialog({
properties: ['openFile'],
filters: [{ name: 'Zip', extensions: ['zip'] }],
});
if (result.canceled || !result.filePaths[0]) {
return { canceled: true };
}
const routingResult = await processZip(result.filePaths[0], config);
return { canceled: false, ...routingResult };
});
ipcMain.handle('get-config-path', async () => ({
configPath: loadConfig.getConfigPath(),
}));

4279
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "progetto",
"version": "1.0.0",
"description": "CAD File Router MVP",
"main": "main.js",
"scripts": {
"dev": "env -u ELECTRON_RUN_AS_NODE electron .",
"start": "env -u ELECTRON_RUN_AS_NODE electron .",
"build": "electron-builder"
},
"keywords": [],
"author": "",
"license": "ISC",
"build": {
"appId": "com.cad.router",
"win": {
"target": "nsis"
}
},
"dependencies": {
"electron": "^40.7.0",
"fs-extra": "^11.3.4",
"unzipper": "^0.12.3"
},
"devDependencies": {
"electron-builder": "^26.8.1"
}
}

7
preload.js Normal file
View File

@@ -0,0 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('api', {
selectFolder: () => ipcRenderer.invoke('select-folder'),
selectZip: () => ipcRenderer.invoke('select-zip'),
getConfigPath: () => ipcRenderer.invoke('get-config-path'),
});

115
renderer/index.html Normal file
View File

@@ -0,0 +1,115 @@
<!doctype html>
<html lang="it">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CAD File Router</title>
<style>
:root {
color-scheme: light;
--bg: #f6f8fb;
--card: #ffffff;
--text: #0f172a;
--muted: #475569;
--accent: #0b5fff;
--border: #dbe2ea;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: linear-gradient(130deg, #eaf0ff 0%, var(--bg) 45%, #f9fbff 100%);
color: var(--text);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
.page {
max-width: 820px;
margin: 40px auto;
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 26px;
}
h1 {
margin-top: 0;
margin-bottom: 6px;
}
p {
margin: 0;
color: var(--muted);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-top: 18px;
}
button {
border: 1px solid transparent;
background: var(--accent);
color: white;
border-radius: 10px;
padding: 11px 16px;
font-size: 15px;
cursor: pointer;
}
button.secondary {
background: #1f2937;
}
button:hover {
opacity: 0.92;
}
button:disabled {
opacity: 0.5;
cursor: wait;
}
.info {
margin-top: 18px;
padding: 14px;
border-radius: 10px;
border: 1px solid var(--border);
background: #f8fafc;
font-size: 14px;
}
pre {
margin-top: 18px;
padding: 14px;
border-radius: 10px;
border: 1px solid var(--border);
background: #0f172a;
color: #e2e8f0;
overflow: auto;
min-height: 180px;
}
</style>
</head>
<body>
<main class="page">
<h1>CAD File Router MVP</h1>
<p>Seleziona una cartella o uno ZIP. I file .prt/.asm/.drw verranno copiati in base alle regole.</p>
<div class="actions">
<button id="folderBtn">Process Folder</button>
<button id="zipBtn" class="secondary">Process ZIP</button>
</div>
<div class="info" id="configInfo">Caricamento configurazione...</div>
<pre id="output">Pronto.</pre>
</main>
<script src="./renderer.js"></script>
</body>
</html>

55
renderer/renderer.js Normal file
View File

@@ -0,0 +1,55 @@
const folderBtn = document.getElementById('folderBtn');
const zipBtn = document.getElementById('zipBtn');
const output = document.getElementById('output');
const configInfo = document.getElementById('configInfo');
function setLoading(isLoading) {
folderBtn.disabled = isLoading;
zipBtn.disabled = isLoading;
}
function renderResult(title, result) {
const details = (result.details || []).slice(0, 20);
const detailsText = details.length
? details
.map((d) => (d.destination ? `- ${d.file} -> ${d.destination}` : `- ${d.file}: ${d.reason}`))
.join('\n')
: '- nessun dettaglio';
output.textContent = [
title,
`scansionati: ${result.scanned ?? 0}`,
`copiati: ${result.copied ?? 0}`,
`saltati: ${result.skipped ?? 0}`,
'',
'dettagli (max 20):',
detailsText,
].join('\n');
}
async function handleAction(actionName, actionFn) {
setLoading(true);
output.textContent = `${actionName} in corso...`;
try {
const result = await actionFn();
if (!result || result.canceled) {
output.textContent = `${actionName} annullato.`;
return;
}
renderResult(actionName, result);
} catch (error) {
output.textContent = `${actionName} fallito:\n${error.message}`;
} finally {
setLoading(false);
}
}
folderBtn.addEventListener('click', () => handleAction('Process Folder', window.api.selectFolder));
zipBtn.addEventListener('click', () => handleAction('Process ZIP', window.api.selectZip));
window.api.getConfigPath().then(({ configPath }) => {
configInfo.textContent = `Config utente: ${configPath}`;
});

20
services/configService.js Normal file
View File

@@ -0,0 +1,20 @@
const fs = require('fs-extra');
const path = require('path');
const os = require('os');
const configPath = path.join(os.homedir(), '.cad-router-config.json');
const defaultConfigPath = path.join(__dirname, '..', 'config', 'defaultConfig.json');
async function loadConfig() {
if (await fs.pathExists(configPath)) {
return fs.readJson(configPath);
}
const defaultConfig = await fs.readJson(defaultConfigPath);
await fs.writeJson(configPath, defaultConfig, { spaces: 2 });
return defaultConfig;
}
loadConfig.getConfigPath = () => configPath;
module.exports = { loadConfig };

View File

@@ -0,0 +1,47 @@
const fs = require('fs-extra');
const path = require('path');
const { findDestination, isCadFile } = require('./router');
async function processFolder(folder, config) {
const entries = await fs.readdir(folder, { withFileTypes: true });
const result = {
scanned: 0,
copied: 0,
skipped: 0,
details: [],
};
for (const entry of entries) {
if (!entry.isFile()) {
continue;
}
const file = entry.name;
result.scanned += 1;
if (!isCadFile(file)) {
result.skipped += 1;
continue;
}
const src = path.join(folder, file);
const destDir = findDestination(file, config);
if (!destDir) {
result.skipped += 1;
result.details.push({ file, reason: 'Nessuna regola trovata' });
continue;
}
await fs.ensureDir(destDir);
const dest = path.join(destDir, file);
await fs.copy(src, dest, { overwrite: true });
result.copied += 1;
result.details.push({ file, destination: destDir });
}
return result;
}
module.exports = { processFolder };

29
services/router.js Normal file
View File

@@ -0,0 +1,29 @@
const path = require('path');
function findDestination(filename, config) {
const ext = path.extname(filename).slice(1).toLowerCase();
for (const rule of config.rules || []) {
if ((rule.ext || '').toLowerCase() !== ext) {
continue;
}
if (!rule.pattern) {
return rule.destination;
}
const regex = new RegExp(rule.pattern);
if (regex.test(path.basename(filename))) {
return rule.destination;
}
}
return null;
}
function isCadFile(filename) {
const ext = path.extname(filename).slice(1).toLowerCase();
return ['prt', 'asm', 'drw'].includes(ext);
}
module.exports = { findDestination, isCadFile };

52
services/zipProcessor.js Normal file
View File

@@ -0,0 +1,52 @@
const unzipper = require('unzipper');
const fs = require('fs-extra');
const path = require('path');
const { pipeline } = require('stream/promises');
const { findDestination, isCadFile } = require('./router');
async function processZip(zipPath, config) {
const stream = fs.createReadStream(zipPath).pipe(unzipper.Parse({ forceStream: true }));
const result = {
scanned: 0,
copied: 0,
skipped: 0,
details: [],
};
for await (const entry of stream) {
if (entry.type !== 'File') {
entry.autodrain();
continue;
}
const file = entry.path;
const baseName = path.basename(file);
result.scanned += 1;
if (!isCadFile(baseName)) {
result.skipped += 1;
entry.autodrain();
continue;
}
const destDir = findDestination(baseName, config);
if (!destDir) {
result.skipped += 1;
result.details.push({ file: baseName, reason: 'Nessuna regola trovata' });
entry.autodrain();
continue;
}
await fs.ensureDir(destDir);
const dest = path.join(destDir, baseName);
await pipeline(entry, fs.createWriteStream(dest));
result.copied += 1;
result.details.push({ file: baseName, destination: destDir });
}
return result;
}
module.exports = { processZip };

396
sop.md Normal file
View File

@@ -0,0 +1,396 @@
# SOP CAD File Router MVP
## Obiettivo
Realizzare un'app desktop (.exe) che:
- accetti una cartella o uno ZIP
- legga file CAD (.prt .asm .drw)
- analizzi nome file ed estensione
- applichi regole di routing
- copi i file verso destinazioni configurate (anche share di rete)
Tecnologie:
- Node.js
- Electron
- unzipper
- fs-extra
---
## 1. Prerequisiti
Installare:
- Node.js LTS
- Git
- Visual Studio Code / VSCodium
Verifica:
```
node -v
npm -v
```
---
## 2. Creazione progetto
```
mkdir cad-file-router
cd cad-file-router
npm init -y
```
Installare dipendenze:
```
npm install electron unzipper fs-extra
npm install electron-builder --save-dev
```
---
## 3. Struttura progetto
```
cad-file-router
├── package.json
├── main.js
├── preload.js
├── renderer
│ ├── index.html
│ └── renderer.js
├── services
│ ├── router.js
│ ├── zipProcessor.js
│ ├── folderProcessor.js
│ └── configService.js
└── config
└── defaultConfig.json
```
---
## 4. Configurazione iniziale
`config/defaultConfig.json`
```
{
"rules": [
{
"ext": "prt",
"pattern": "^1",
"destination": "\\\\SERVER-CAD\\clienti1\\PRT"
},
{
"ext": "prt",
"pattern": "^6",
"destination": "\\\\SERVER-CAD\\clienti2\\PRT"
},
{
"ext": "asm",
"destination": "\\\\SERVER-CAD\\assembly"
},
{
"ext": "drw",
"destination": "\\\\SERVER-CAD\\drawing"
}
]
}
```
---
## 5. Config Service
`services/configService.js`
```
const fs = require("fs-extra")
const path = require("path")
const os = require("os")
const configPath = path.join(os.homedir(), ".cad-router-config.json")
function loadConfig(){
if(fs.existsSync(configPath)){
return fs.readJsonSync(configPath)
}
const defaultConfig = fs.readJsonSync(
path.join(__dirname,"../config/defaultConfig.json")
)
fs.writeJsonSync(configPath, defaultConfig)
return defaultConfig
}
module.exports = { loadConfig }
```
---
## 6. Motore di routing
`services/router.js`
```
const path = require("path")
function findDestination(filename, config){
const ext = path.extname(filename).slice(1)
for(const rule of config.rules){
if(rule.ext !== ext)
continue
if(!rule.pattern)
return rule.destination
const regex = new RegExp(rule.pattern)
if(regex.test(filename))
return rule.destination
}
return null
}
module.exports = { findDestination }
```
---
## 7. Processare una cartella
`services/folderProcessor.js`
```
const fs = require("fs-extra")
const path = require("path")
const { findDestination } = require("./router")
async function processFolder(folder, config){
const files = await fs.readdir(folder)
for(const file of files){
const src = path.join(folder,file)
const destDir = findDestination(file, config)
if(!destDir) continue
const dest = path.join(destDir,file)
await fs.copy(src,dest)
}
}
module.exports = { processFolder }
```
---
## 8. Processare ZIP
`services/zipProcessor.js`
```
const unzipper = require("unzipper")
const fs = require("fs-extra")
const path = require("path")
const { findDestination } = require("./router")
async function processZip(zipPath, config){
const stream = fs.createReadStream(zipPath)
.pipe(unzipper.Parse())
for await (const entry of stream){
const file = entry.path
const destDir = findDestination(file, config)
if(!destDir){
entry.autodrain()
continue
}
const dest = path.join(destDir,path.basename(file))
entry.pipe(fs.createWriteStream(dest))
}
}
module.exports = { processZip }
```
---
## 9. Main Electron
`main.js`
```
const { app, BrowserWindow, dialog, ipcMain } = require("electron")
const path = require("path")
const { processFolder } = require("./services/folderProcessor")
const { processZip } = require("./services/zipProcessor")
const { loadConfig } = require("./services/configService")
let config = loadConfig()
function createWindow(){
const win = new BrowserWindow({
width:800,
height:600,
webPreferences:{
preload: path.join(__dirname,"preload.js")
}
})
win.loadFile("renderer/index.html")
}
app.whenReady().then(createWindow)
ipcMain.handle("select-folder", async () => {
const result = await dialog.showOpenDialog({
properties:["openDirectory"]
})
if(result.canceled) return
await processFolder(result.filePaths[0], config)
})
ipcMain.handle("select-zip", async () => {
const result = await dialog.showOpenDialog({
filters:[{ name:"Zip", extensions:["zip"] }]
})
if(result.canceled) return
await processZip(result.filePaths[0], config)
})
```
---
## 10. Preload
`preload.js`
```
const { contextBridge, ipcRenderer } = require("electron")
contextBridge.exposeInMainWorld("api", {
selectFolder: () => ipcRenderer.invoke("select-folder"),
selectZip: () => ipcRenderer.invoke("select-zip")
})
```
---
## 11. UI minima
`renderer/index.html`
```
<h2>CAD File Router</h2>
<button onclick="window.api.selectFolder()">
Process Folder
</button>
<button onclick="window.api.selectZip()">
Process ZIP
</button>
```
---
## 12. Avvio applicazione
Nel package.json aggiungere:
```
"scripts": {
"start": "electron ."
}
```
Avviare:
```
npm start
```
---
## 13. Build .exe
Aggiungere nel package.json:
```
"build": {
"appId": "com.cad.router",
"win": {
"target": "nsis"
}
}
```
Build:
```
npx electron-builder
```
Output:
```
dist/
cad-file-router Setup.exe
```
---
## MVP completato quando
- l'app si avvia
- si seleziona ZIP o cartella
- i file .prt .asm .drw vengono analizzati
- i file vengono copiati nelle destinazioni configurate
---
## Migliorie future
- progress bar
- drag and drop ZIP
- modifica regole da UI
- logging operazioni
- parallelismo file
- watch folder automatico