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: {
|
||||
type: DataTypes.INTEGER,
|
||||
type: DataTypes.BIGINT,
|
||||
|
||||
|
||||
|
||||
|
||||
@ -29,14 +29,14 @@ content: {
|
||||
},
|
||||
|
||||
size_bytes: {
|
||||
type: DataTypes.INTEGER,
|
||||
type: DataTypes.BIGINT,
|
||||
|
||||
|
||||
|
||||
},
|
||||
|
||||
epoch_millis: {
|
||||
type: DataTypes.INTEGER,
|
||||
type: DataTypes.BIGINT,
|
||||
|
||||
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const app = express();
|
||||
@ -87,7 +86,8 @@ app.use('/api-docs', function (req, res, next) {
|
||||
app.use(cors({origin: true}));
|
||||
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/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 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({
|
||||
storage: Multer.memoryStorage(),
|
||||
@ -8,4 +10,4 @@ let processFile = Multer({
|
||||
}).single("file");
|
||||
|
||||
let processFileMiddleware = util.promisify(processFile);
|
||||
module.exports = processFileMiddleware;
|
||||
module.exports = processFileMiddleware;
|
||||
@ -7,7 +7,12 @@ const menuAside: MenuAsideItem[] = [
|
||||
icon: icon.mdiViewDashboardOutline,
|
||||
label: 'Dashboard',
|
||||
},
|
||||
|
||||
{
|
||||
href: '/text_files/processor',
|
||||
label: 'Epoch Processor',
|
||||
icon: icon.mdiFileDocumentEdit,
|
||||
permissions: 'CREATE_TEXT_FILES'
|
||||
},
|
||||
{
|
||||
href: '/users/users-list',
|
||||
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 { getPageTitle } from '../config'
|
||||
import Link from "next/link";
|
||||
import BaseButton from '../components/BaseButton';
|
||||
|
||||
import { hasPermission } from "../helpers/userPermissions";
|
||||
import { fetchWidgets } from '../stores/roles/rolesSlice';
|
||||
@ -98,6 +99,24 @@ const Dashboard = () => {
|
||||
main>
|
||||
{''}
|
||||
</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
|
||||
currentUser={currentUser}
|
||||
@ -378,4 +397,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
|
||||
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