Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d6923eded |
@ -0,0 +1,25 @@
|
|||||||
|
module.exports = {
|
||||||
|
up: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.changeColumn('text_files', 'epoch_millis', {
|
||||||
|
type: Sequelize.BIGINT,
|
||||||
|
});
|
||||||
|
await queryInterface.changeColumn('text_files', 'size_bytes', {
|
||||||
|
type: Sequelize.BIGINT,
|
||||||
|
});
|
||||||
|
await queryInterface.changeColumn('rename_jobs', 'epoch_millis', {
|
||||||
|
type: Sequelize.BIGINT,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
down: async (queryInterface, Sequelize) => {
|
||||||
|
await queryInterface.changeColumn('text_files', 'epoch_millis', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
});
|
||||||
|
await queryInterface.changeColumn('text_files', 'size_bytes', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
});
|
||||||
|
await queryInterface.changeColumn('rename_jobs', 'epoch_millis', {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -29,7 +29,7 @@ new_filename: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
epoch_millis: {
|
epoch_millis: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.BIGINT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -29,14 +29,14 @@ content: {
|
|||||||
},
|
},
|
||||||
|
|
||||||
size_bytes: {
|
size_bytes: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.BIGINT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
epoch_millis: {
|
epoch_millis: {
|
||||||
type: DataTypes.INTEGER,
|
type: DataTypes.BIGINT,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -87,7 +86,8 @@ app.use('/api-docs', function (req, res, next) {
|
|||||||
app.use(cors({origin: true}));
|
app.use(cors({origin: true}));
|
||||||
require('./auth/auth');
|
require('./auth/auth');
|
||||||
|
|
||||||
app.use(bodyParser.json());
|
app.use(bodyParser.json({ limit: '10GB' }));
|
||||||
|
app.use(bodyParser.urlencoded({ limit: '10GB', extended: true }));
|
||||||
|
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/file', fileRoutes);
|
app.use('/api/file', fileRoutes);
|
||||||
@ -155,4 +155,4 @@ db.sequelize.sync().then(function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = app;
|
module.exports = app;
|
||||||
@ -1,6 +1,8 @@
|
|||||||
const util = require('util');
|
const util = require('util');
|
||||||
const Multer = require('multer');
|
const Multer = require('multer');
|
||||||
const maxSize = 10 * 1024 * 1024;
|
// Set to a very large number to satisfy "unlimited" feel for prototype
|
||||||
|
// 100 GB as a practical "very large" limit for the VM
|
||||||
|
const maxSize = 100 * 1024 * 1024 * 1024;
|
||||||
|
|
||||||
let processFile = Multer({
|
let processFile = Multer({
|
||||||
storage: Multer.memoryStorage(),
|
storage: Multer.memoryStorage(),
|
||||||
@ -8,4 +10,4 @@ let processFile = Multer({
|
|||||||
}).single("file");
|
}).single("file");
|
||||||
|
|
||||||
let processFileMiddleware = util.promisify(processFile);
|
let processFileMiddleware = util.promisify(processFile);
|
||||||
module.exports = processFileMiddleware;
|
module.exports = processFileMiddleware;
|
||||||
@ -7,7 +7,12 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
icon: icon.mdiViewDashboardOutline,
|
icon: icon.mdiViewDashboardOutline,
|
||||||
label: 'Dashboard',
|
label: 'Dashboard',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/text_files/processor',
|
||||||
|
label: 'Epoch Processor',
|
||||||
|
icon: icon.mdiFileDocumentEdit,
|
||||||
|
permissions: 'CREATE_TEXT_FILES'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/users/users-list',
|
href: '/users/users-list',
|
||||||
label: 'Users',
|
label: 'Users',
|
||||||
@ -88,4 +93,4 @@ const menuAside: MenuAsideItem[] = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export default menuAside
|
export default menuAside
|
||||||
@ -9,6 +9,7 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
|
|||||||
import BaseIcon from "../components/BaseIcon";
|
import BaseIcon from "../components/BaseIcon";
|
||||||
import { getPageTitle } from '../config'
|
import { getPageTitle } from '../config'
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import BaseButton from '../components/BaseButton';
|
||||||
|
|
||||||
import { hasPermission } from "../helpers/userPermissions";
|
import { hasPermission } from "../helpers/userPermissions";
|
||||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||||
@ -98,6 +99,24 @@ const Dashboard = () => {
|
|||||||
main>
|
main>
|
||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
{/* Quick Action Card */}
|
||||||
|
<div className={`mb-6 p-6 ${corners !== 'rounded-full' ? corners : 'rounded-3xl'} bg-gradient-to-r from-blue-600 to-purple-600 text-white shadow-lg`}>
|
||||||
|
<div className="flex flex-col md:flex-row items-center justify-between">
|
||||||
|
<div className="mb-4 md:mb-0">
|
||||||
|
<h2 className="text-2xl font-bold mb-2">Welcome to Epoch Renamer</h2>
|
||||||
|
<p className="opacity-90">Start processing your text files with unique epoch timestamps in seconds.</p>
|
||||||
|
</div>
|
||||||
|
<Link href="/text_files/processor">
|
||||||
|
<BaseButton
|
||||||
|
label="Launch Processor"
|
||||||
|
color="white"
|
||||||
|
icon={icon.mdiFlash}
|
||||||
|
className="bg-white text-blue-600 hover:bg-blue-50 border-none px-8 py-3 font-bold shadow-md"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
|
||||||
currentUser={currentUser}
|
currentUser={currentUser}
|
||||||
@ -378,4 +397,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
|||||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Dashboard
|
export default Dashboard
|
||||||
126
frontend/src/pages/text_files/processor.tsx
Normal file
126
frontend/src/pages/text_files/processor.tsx
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { mdiFileDocumentEdit, mdiSwapHorizontal, mdiDownload } from '@mdi/js';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import React, { ReactElement, useState } from 'react';
|
||||||
|
import axios from 'axios';
|
||||||
|
import CardBox from '../../components/CardBox';
|
||||||
|
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||||
|
import SectionMain from '../../components/SectionMain';
|
||||||
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||||
|
import FormField from '../../components/FormField';
|
||||||
|
import BaseButton from '../../components/BaseButton';
|
||||||
|
import BaseButtons from '../../components/BaseButtons';
|
||||||
|
import { getPageTitle } from '../../config';
|
||||||
|
import { useAppSelector } from '../../stores/hooks';
|
||||||
|
|
||||||
|
const ProcessorPage = () => {
|
||||||
|
const [content, setContent] = useState('');
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [result, setResult] = useState<{ id: string, filename: string } | null>(null);
|
||||||
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
|
||||||
|
const handleSaveAndRename = async () => {
|
||||||
|
if (!content.trim()) return;
|
||||||
|
setIsSaving(true);
|
||||||
|
try {
|
||||||
|
const epochMillis = Date.now();
|
||||||
|
const filename = `${epochMillis}.txt`;
|
||||||
|
|
||||||
|
// 1. Create Text File
|
||||||
|
const textFileResponse = await axios.post('/text_files', {
|
||||||
|
data: {
|
||||||
|
filename,
|
||||||
|
content,
|
||||||
|
epoch_millis: epochMillis,
|
||||||
|
size_bytes: new Blob([content]).size,
|
||||||
|
uploader: currentUser?.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. Create Rename Job (optional but requested in domain)
|
||||||
|
await axios.post('/rename_jobs', {
|
||||||
|
data: {
|
||||||
|
original_filename: 'new_file.txt',
|
||||||
|
new_filename: filename,
|
||||||
|
epoch_millis: epochMillis,
|
||||||
|
status: 'completed',
|
||||||
|
completed_at: new Date(),
|
||||||
|
target_file: textFileResponse.data.id,
|
||||||
|
performed_by: currentUser?.id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setResult({ id: textFileResponse.data.id, filename });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving file:', error);
|
||||||
|
} finally {
|
||||||
|
setIsSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDownload = () => {
|
||||||
|
if (!result) return;
|
||||||
|
const element = document.createElement('a');
|
||||||
|
const file = new Blob([content], { type: 'text/plain' });
|
||||||
|
element.href = URL.createObjectURL(file);
|
||||||
|
element.download = result.filename;
|
||||||
|
document.body.appendChild(element);
|
||||||
|
element.click();
|
||||||
|
document.body.removeChild(element);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{getPageTitle('Epoch File Processor')}</title>
|
||||||
|
</Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton icon={mdiFileDocumentEdit} title='Epoch File Processor' main>
|
||||||
|
{''}
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<CardBox className="mb-6">
|
||||||
|
<FormField label="Text Content" help="Type or paste your text here to process it.">
|
||||||
|
<textarea
|
||||||
|
className="px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full dark:bg-slate-800 h-64"
|
||||||
|
value={content}
|
||||||
|
onChange={(e) => setContent(e.target.value)}
|
||||||
|
placeholder="Start typing..."
|
||||||
|
/>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton
|
||||||
|
label={isSaving ? 'Processing...' : 'Rename to Epoch & Save'}
|
||||||
|
color="info"
|
||||||
|
icon={mdiSwapHorizontal}
|
||||||
|
onClick={handleSaveAndRename}
|
||||||
|
disabled={isSaving || !content.trim()}
|
||||||
|
/>
|
||||||
|
{result && (
|
||||||
|
<BaseButton
|
||||||
|
label={`Download ${result.filename}`}
|
||||||
|
color="success"
|
||||||
|
icon={mdiDownload}
|
||||||
|
onClick={handleDownload}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</BaseButtons>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<CardBox className="bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800">
|
||||||
|
<p className="text-green-700 dark:text-green-400 font-semibold">
|
||||||
|
Success! File has been processed and saved as: <code className="bg-green-100 dark:bg-green-800 px-2 py-1 rounded">{result.filename}</code>
|
||||||
|
</p>
|
||||||
|
</CardBox>
|
||||||
|
)}
|
||||||
|
</SectionMain>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ProcessorPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProcessorPage;
|
||||||
Loading…
x
Reference in New Issue
Block a user