server.ts (5998B)
1 // Cropyright (C) Ryan Jeffrey 2022 2 // A simple express server that uses handlebars. 3 4 import express from 'express'; 5 import path from 'path'; 6 import exphbs, { engine } from 'express-handlebars'; 7 import fs from 'fs'; 8 9 const app = express(); 10 const port = process.env.PORT || 3000; 11 12 function getMonthByNumber(i: number) : string { 13 const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 14 'Aug', 'Sep', 'Oct', 'Nov', 'Dec' ]; 15 return (i in months) ? months[i] : ""; 16 } 17 18 function permissionToString(i: number) : string { 19 // Unix file permission array. The mode is the index in the array. 20 const permStrings = ['---', '--x', '-w-', '-wx', 'r--', 'r-x', 'rw-', 'rwx']; 21 return (i in permStrings) ? permStrings[i] : ""; 22 } 23 24 class LSStat { 25 perms: string; 26 numLinks: number; 27 fileSize: number; 28 mtime: string; 29 basename: string; 30 31 constructor(thePath: string) { 32 let stats = fs.statSync(thePath); 33 34 // ls file permissions. Convert into an easier format to use. 35 if(!stats) { 36 console.error("Could not stat", thePath); 37 return; 38 } 39 // Convert mode to string. 40 let unixFilePermissions = (stats.mode & parseInt('777', 8)).toString(8); 41 let permsResult = permissionToString(parseInt(unixFilePermissions[0])); 42 permsResult += permissionToString(parseInt(unixFilePermissions[1])); 43 permsResult += permissionToString(parseInt(unixFilePermissions[2])); 44 45 let prefixChar = '-'; 46 if(stats.isDirectory()) { 47 prefixChar = 'd'; 48 } 49 50 this.perms = `${prefixChar}${permsResult}`; 51 this.numLinks = stats.nlink; 52 this.fileSize = stats.size; 53 this.mtime = LSStat.lsTime(stats.mtimeMs); 54 this.basename = thePath; 55 } 56 57 // Get the mtime in the same format that LS would. 58 static lsTime(timeMS: number) : string { 59 let fileDate = new Date(timeMS); 60 let addString = ""; 61 // If the file was updated this year then set the last column to the 62 // hour and minute. Else, the last column should be the year. 63 if((new Date()).getFullYear() != fileDate.getFullYear()) 64 addString = `${fileDate.getHours()}:${fileDate.getMinutes()}`; 65 else 66 addString = ` ${fileDate.getFullYear()}`; 67 return `${getMonthByNumber(fileDate.getMonth())} ${fileDate.getDate()} ${addString}`; 68 } 69 70 static lsList(theDir: string, ext: string, files: string[]) : LSStat[] { 71 let fileStats: LSStat[] = []; 72 files.forEach((element: string) => { 73 fileStats.push(new LSStat(path.join(theDir, element + ext))); 74 }); 75 return fileStats; 76 } 77 78 static lsDir(thePath: string) : LSStat[] { 79 let fileStats: LSStat[] = []; 80 // TODO error checking. 81 let files = fs.readdirSync(thePath); 82 83 files.forEach((file) => { 84 fileStats.push(new LSStat(file)); 85 }); 86 87 return fileStats; 88 } 89 90 static fileExistsIn(thePath: string, statList: LSStat[]) : boolean { 91 for(let i = 0; i < statList.length; i++) { 92 if(statList[i].basename == thePath) 93 return true; 94 } 95 return false; 96 } 97 } 98 99 // Dummy class just for inheritance. 100 class Command { 101 args: string; 102 constructor(args: string) { 103 this.args = args; 104 } 105 } 106 107 class LS extends Command { 108 lsList: LSStat[]; 109 110 constructor(dir: string, ext:string, names: string[]) { 111 super(dir); 112 this.lsList = LSStat.lsList(dir, ext, names); 113 } 114 } 115 116 class Cat extends Command { 117 markup: string; 118 119 constructor(path: string) { 120 super(path); 121 this.markup = fs.readFileSync(path, 'utf8'); 122 } 123 } 124 125 class TerminalWindow { 126 commands: Command[]; 127 128 constructor(...commands: Command[]) { 129 this.commands = commands; 130 } 131 } 132 133 // App config 134 135 app.engine('handlebars', engine({ defaultLayout: 'main' })); 136 app.set('view engine', 'handlebars'); 137 app.set('views', "./views"); 138 139 app.use(express.static(path.join(process.cwd(), 'public'))); 140 app.use(express.static(path.join(process.cwd(), 'external'))); 141 app.use(express.json()); 142 143 // TODO maybe a system that exports org to handlebars. 144 const postItems = LSStat.lsDir('posts'); 145 // Get the requested post 146 app.get('/posts/:post', (req, res, next) => { 147 let post = req.params.post.toLowerCase(); 148 if(LSStat.fileExistsIn(post, postItems)) { 149 res.status(200).render('writing', { text : fs.readFileSync(post) }); 150 } 151 else { 152 // Page not found. 153 res.status(404).render('404'); 154 } 155 }); 156 // Posts index file. 157 app.get('/posts', (req, res, next) => { 158 res.status(200).render('indexWriting'); 159 }); 160 // index.html should be before 404 and after everything else 161 // Generate files object. 162 const files = LSStat.lsDir('public/files'); 163 // Server entry for files. 164 app.get('/files', (req, res, next) => { 165 res.status(200).render('files', { 166 entries: files 167 }); 168 }); 169 170 // LS everything. 171 const frontPageItems = LSStat.lsList('.', '.html', ['main', 'software', 'sneed']); 172 173 app.get('/:item', (req, res, next) => { 174 let item = req.params.item.toLowerCase(); 175 if(LSStat.fileExistsIn(item, frontPageItems)) { 176 res.status(200).render('post', { text : fs.readFileSync(item) }); 177 } 178 else { 179 // Page not found. 180 res.status(404).render('404'); 181 } 182 }); 183 184 // TODO!!!! each window needs multiple commands. 185 app.get('/', (req, res, next) => { 186 res.status(200).render('index', { 187 windows: [new TerminalWindow(new Cat('public/figlet.html'), 188 new LS('.', '.html', ['main', 'software', 'sneed']), 189 new Cat('public/front.html'))], 190 }); 191 }); 192 193 // 404 is last. 194 app.get('*', (req, res) => { 195 res.status(404).render('404'); 196 }); 197 198 // Server initialize. 199 app.listen(port, () => { 200 console.log('== Server is listening on port', port, 201 'in current directory', process.cwd()); 202 });