Skip to content
Permalink
Browse files

Working prototype using the filesystem-backed database

  • Loading branch information...
thesephist committed Oct 6, 2018
1 parent 84db983 commit f7198bd8a57b61a9c814389534943e791356f02d
Showing with 645 additions and 2 deletions.
  1. +4 −0 .gitignore
  2. +4 −2 README.md
  3. +5 −0 config.js
  4. +18 −0 package.json
  5. +81 −0 src/main.js
  6. +136 −0 src/storage.js
  7. +22 −0 src/utils.js
  8. 0 static/css/main.css
  9. +17 −0 static/index.html
  10. +358 −0 yarn.lock
@@ -59,3 +59,7 @@ typings/

# next.js build output
.next

# database folder for linus.zone
db/

@@ -1,2 +1,4 @@
# zone
A URL shortener / note sharing service. linus.zone
# linus.zone

A URL shortener / note sharing service, running at [linus.zone](linus.zone).

@@ -0,0 +1,5 @@
module.exports = {
PORT: 3000,
DATABASE: 'db',
}

@@ -0,0 +1,18 @@
{
"name": "zone",
"version": "1.0.0",
"description": "linus.zone",
"main": "src/main.js",
"repository": "git@github.com:thesephist/zone.git",
"author": "Linus Lee <hi@linus.zone>",
"license": "MIT",
"scripts": {
"start": "node src/main.js"
},
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.3",
"marked": "^0.5.1",
"shortid": "^2.2.13"
}
}
@@ -0,0 +1,81 @@
const fs = require('fs');
const path = require('path');

const bodyParser = require('body-parser');
const express = require('express');
const app = express();
app.use(bodyParser.urlencoded({
extended: false,
}));

const Config = require('../config.js');
const { StorageBackend, Record } = require('./storage.js');

// create db if not exists
if (!fs.existsSync(Config.DATABASE)) {
fs.mkdirSync(Config.DATABASE);
}

const storage = new StorageBackend(Config.DATABASE);

app.get('/', (req, res) => {
fs.readFile('static/index.html', 'utf8', (err, data) => {
if (err) {
throw err;
}

res.set('Content-Type', 'text/html');
res.send(data);
});
});

app.post('/new', async (req, res) => {
try {
const record = new Record({
id: req.body.id || undefined,
type: req.body.content_uri ? 'uri' : 'note',
content: req.body.content_uri || req.body.content_note,
});

if (record.isURI() && record.id === record.content) {
res.status(409);
res.send(`This note will create a redirect loop!`);
return;
}

await storage.save(record);
res.redirect(302, `/${record.id}`);
console.log(`Created note ${record.id} as ${record.type}`);
} catch (e) {
res.status(500);
res.send('');
console.log(`Error on /new: ${e}`);
}
});

app.get('/:id', async (req, res) => {
res.set('Content-Type', 'text/html');

const rid = req.params.id;
if (await storage.has(rid)) {
const record = await storage.get(rid);
if (record.isNote()) {
res.send(record.render());
console.log(`Rendered note ${record.id} as HTML`);
} else if (record.isURI()) {
res.redirect(302, record.getRedirect());
console.log(`Redirected note ${record.id} to ${record.getRedirect()}`);
}
} else {
res.status(404);
res.send(`Record ${rid} does not exist.`);
}
});

app.use('/static', express.static('static'));

app.listen(
Config.PORT,
_ => console.log(`Zone service running on :${Config.PORT}`)
);

@@ -0,0 +1,136 @@
const fs = require('fs');
const path = require('path');

const marked = require('marked');
const { randid, validid } = require('./utils.js');

class StorageBackend {

constructor(dirpath) {
this.dirpath = dirpath;
}

pathFromID(id) {
return path.join(this.dirpath, id);
}

has(id) {
return new Promise((res, rej) => {
fs.access(
this.pathFromID(id),
fs.constants.F_OK,
err => {
res(!err);
}
);
});
}

get(id) {
return new Promise((res, rej) => {
fs.readFile(
this.pathFromID(id),
'utf8',
(err, data) => {
if (err) {
rej(err);
} else {
res(Record.parse(data));
}
});
});
}

save(record) {
return new Promise((res, rej) => {
fs.writeFile(
this.pathFromID(record.id),
record.serialize(),
'utf8',
(err) => {
if (err) {
rej(err);
} else {
res();
}
}
)
})
}

}

class Record {

constructor({
id = randid(),
type = 'note',
content = '',
}) {
this.id = id.trim();
this.type = type;
this.content = content.trim();
}

isURI() {
return this.type === 'uri';
}

isNote() {
return this.type === 'note';
}

validate() {
const VALID_TYPES = [
'uri',
'note',
];

return [
validid(this.id),
VALID_TYPES.includes(this.type),
this.content !== '',
].some(x => x);
}

render() {
if (!this.isNote()) {
throw new Error(`Cannot render a record of type ${this.type}`);
}

return marked(this.content);
}

getRedirect() {
if (!this.isURI()) {
throw new Error(`Cannot render a redirect path for record of type ${this.type}`);
}

return this.content;
}

save(storage) {
storage.save(this);
}

serialize() {
return `${this.type}.${this.id}\n${this.content}`;
}

static parse(data) {
const [firstline, ...contentLines] = data.split('\n');
const [type, id] = firstline.split('.');
return new Record({
id: id,
type: type,
content: contentLines.join('\n'),
});
}

}

module.exports = {
StorageBackend,
Record,
}

@@ -0,0 +1,22 @@
const path = require('path');
const shortid = require('shortid');

/**
* Get a random ID for a record
*/
const randid = _ => {
return shortid.generate();
}

/**
* Is the given ID a valid ID in our system?
*/
const validid = id => {
return shortid.validate(id);
}

module.exports = {
randid,
validid,
}

No changes.
@@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<title>linus.zone</title>
</head>
<body>
<h1>New</h1>
<form action="/new" method="post">
<input type="text" name="id">
<label for="content_uri">URI</label>
<input type="text" name="content_uri"/>
<label for="content_note">Note</label>
<textarea name="content_note"></textarea>
<input type="submit" value="Submit"/>
</form>
</body>
</html>

0 comments on commit f7198bd

Please sign in to comment.
You can’t perform that action at this time.