diff --git a/backend/src/db/migrations/20260125000000-change-epoch-to-bigint.js b/backend/src/db/migrations/20260125000000-change-epoch-to-bigint.js new file mode 100644 index 0000000..67b9b3e --- /dev/null +++ b/backend/src/db/migrations/20260125000000-change-epoch-to-bigint.js @@ -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, + }); + }, +}; \ No newline at end of file diff --git a/backend/src/db/models/rename_jobs.js b/backend/src/db/models/rename_jobs.js index 289021f..d1c0d9d 100644 --- a/backend/src/db/models/rename_jobs.js +++ b/backend/src/db/models/rename_jobs.js @@ -29,7 +29,7 @@ new_filename: { }, epoch_millis: { - type: DataTypes.INTEGER, + type: DataTypes.BIGINT, diff --git a/backend/src/db/models/text_files.js b/backend/src/db/models/text_files.js index 3853dba..6537549 100644 --- a/backend/src/db/models/text_files.js +++ b/backend/src/db/models/text_files.js @@ -29,14 +29,14 @@ content: { }, size_bytes: { - type: DataTypes.INTEGER, + type: DataTypes.BIGINT, }, epoch_millis: { - type: DataTypes.INTEGER, + type: DataTypes.BIGINT, diff --git a/backend/src/index.js b/backend/src/index.js index 2608616..2a0612c 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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; \ No newline at end of file diff --git a/backend/src/middlewares/upload.js b/backend/src/middlewares/upload.js index ea3e835..c75b018 100644 --- a/backend/src/middlewares/upload.js +++ b/backend/src/middlewares/upload.js @@ -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; \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 7475064..8cc6c9c 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -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 \ No newline at end of file diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index ef0878d..97b72ed 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -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> {''} + + {/* Quick Action Card */} +
+
+
+

Welcome to Epoch Renamer

+

Start processing your text files with unique epoch timestamps in seconds.

+
+ + + +
+
{hasPermission(currentUser, 'CREATE_ROLES') && {page} } -export default Dashboard +export default Dashboard \ No newline at end of file diff --git a/frontend/src/pages/text_files/processor.tsx b/frontend/src/pages/text_files/processor.tsx new file mode 100644 index 0000000..2eb34c0 --- /dev/null +++ b/frontend/src/pages/text_files/processor.tsx @@ -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 ( + <> + + {getPageTitle('Epoch File Processor')} + + + + {''} + + + + +