From 46186807378c2f2b3855ffd8e6a5cde372a1dae6 Mon Sep 17 00:00:00 2001 From: Matthieu Tudury Date: Sat, 21 Nov 2020 22:16:32 +0100 Subject: [PATCH] hastepad 0.5 --- Dockerfile | 54 +++--------- README.md | 30 +++++++ about.md | 14 ++++ config.js => config.json | 6 +- docker-compose.yaml | 23 ++--- docker-entrypoint.js | 106 ++++++++++------------- docker-entrypoint.sh | 2 +- lib/document_handler.js | 117 ++++++++++++++++++++++++-- lib/document_stores/file.js | 32 ++++++- package.json | 11 ++- server.js | 23 ++++- static/application.js | 162 +++++++++++++++++++++++++++++------- static/application.min.js | 2 +- static/index.html | 6 +- 14 files changed, 422 insertions(+), 166 deletions(-) rename config.js => config.json (83%) diff --git a/Dockerfile b/Dockerfile index dd53efc..7b7d5ac 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,56 +3,24 @@ FROM node:14.8.0-stretch RUN mkdir -p /usr/src/app && \ chown node:node /usr/src/app -USER node:node - WORKDIR /usr/src/app -COPY --chown=node:node . . +COPY . . -RUN npm install && \ - npm install redis@0.8.1 && \ - npm install pg@4.1.1 && \ - npm install memcached@2.2.2 && \ - npm install aws-sdk@2.738.0 && \ - npm install rethinkdbdash@2.3.31 +RUN mv config.json base.config.json && touch config.json && chown node:node config.json ./static/application.min.js && mkdir data && chown node:node data -ENV STORAGE_TYPE=memcached \ - STORAGE_HOST=127.0.0.1 \ - STORAGE_PORT=11211\ - STORAGE_EXPIRE_SECONDS=2592000\ - STORAGE_DB=2 \ - STORAGE_AWS_BUCKET= \ - STORAGE_AWS_REGION= \ - STORAGE_USENAMER= \ - STORAGE_PASSWORD= \ - STORAGE_FILEPATH= - -ENV LOGGING_LEVEL=verbose \ - LOGGING_TYPE=Console \ - LOGGING_COLORIZE=true +RUN npm install +# && \ +# npm install redis@0.8.1 && \ +# npm install pg@4.1.1 && \ +# npm install memcached@2.2.2 && \ +# npm install aws-sdk@2.738.0 && \ +# npm install rethinkdbdash@2.3.31 ENV HOST=0.0.0.0\ - PORT=7777\ - KEY_LENGTH=10\ - MAX_LENGTH=400000\ - STATIC_MAX_AGE=86400\ - RECOMPRESS_STATIC_ASSETS=true + PORT=7777 -ENV KEYGENERATOR_TYPE=phonetic \ - KEYGENERATOR_KEYSPACE= - -ENV RATELIMITS_NORMAL_TOTAL_REQUESTS=500\ - RATELIMITS_NORMAL_EVERY_MILLISECONDS=60000 \ - RATELIMITS_WHITELIST_TOTAL_REQUESTS= \ - RATELIMITS_WHITELIST_EVERY_MILLISECONDS= \ - # comma separated list for the whitelisted \ - RATELIMITS_WHITELIST=example1.whitelist,example2.whitelist \ - \ - RATELIMITS_BLACKLIST_TOTAL_REQUESTS= \ - RATELIMITS_BLACKLIST_EVERY_MILLISECONDS= \ - # comma separated list for the blacklisted \ - RATELIMITS_BLACKLIST=example1.blacklist,example2.blacklist -ENV DOCUMENTS=about=./about.md +USER node:node EXPOSE ${PORT} STOPSIGNAL SIGINT diff --git a/README.md b/README.md index b42c844..34fcab2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,13 @@ +# TestNow + +Test it in seconds : + +```bash +docker run --rm -d -p 7777:7777 hastepad:0.5 +``` +Open your browser and type url : http://127.0.0.1:7777/ + + # Haste Haste is an open-source pastebin software written in node.js, which is easily @@ -20,6 +30,26 @@ to do things like: which will output a URL to share containing the contents of `cat something`'s STDOUT. Check the README there for more details and usages. +# Customized version (HastePad) + +This is not the original version, you'll find the original version at : https://github.com/seejohnrun/haste-server + +This version is from here : https://github.com/mtudury/haste-server + +This version is customized in order to add/change some features : + +- Live saving : Do not loose your work when closing your browser +- List documents +- Delete document + +It currently only works with storage type : file. + +It main usage would be like a notepad online (mono user) + +It does not have exactly the same purpose of original haste (no edit/no delete needed nor wanted in original version) so choose accordingly to your needs + +If you need security, i would recommend : add a reverse-proxy in front (Caddy/Nginx), HTTPS and authentication using reverse proxy features, only expose the reverse proxy. + ## Tested Browsers * Firefox 8 diff --git a/about.md b/about.md index c446934..1a03ea6 100644 --- a/about.md +++ b/about.md @@ -6,6 +6,20 @@ use pastebins. Haste is the prettiest, easiest to use pastebin ever made. +## Customized version (HastePad) + +This is not the original version, you'll find the original version at : https://github.com/seejohnrun/haste-server + +This version is from here : https://github.com/mtudury/haste-server + +This version is customized in order to add/change some features : + +- Live saving : Do not loose your work when closing your browser +- List document +- Delete document + +It main usage would be like a notepad online (mono user) + ## Basic Usage Type what you want me to see, click "Save", and then copy the URL. Send that diff --git a/config.js b/config.json similarity index 83% rename from config.js rename to config.json index e5381ed..78652a1 100644 --- a/config.js +++ b/config.json @@ -33,11 +33,13 @@ }, "storage": { - "type": "file" + "type": "file", + "allowList": true, + "allowDelete": true }, "documents": { - "about": "./about.md" + "about.md": "./about.md" } } diff --git a/docker-compose.yaml b/docker-compose.yaml index 8365c5d..352ee74 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,19 +1,20 @@ -version: '3.0' +version: '3.6' services: haste-server: build: . - networks: - - db-network - environment: - - STORAGE_TYPE=memcached - - STORAGE_HOST=memcached - - STORAGE_PORT=11211 + image: hastepad:0.5 ports: - 7777:7777 - memcached: - image: memcached:latest - networks: - - db-network + volumes: + - type: tmpfs + target: /usr/src/app/data + tmpfs: + size: 128M + logging: + driver: "json-file" + options: + max-size: "1M" + max-file: "10" networks: db-network: diff --git a/docker-entrypoint.js b/docker-entrypoint.js index 5afff14..d3702dd 100644 --- a/docker-entrypoint.js +++ b/docker-entrypoint.js @@ -29,80 +29,58 @@ const { RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS, RATE_LIMITS_BLACKLIST, DOCUMENTS, + STORAGE_ALLOWLIST, + STORAGE_ALLOWDELETE } = process.env; -const config = { - host: HOST, - port: PORT, +const config = require("./base.config.json"); - keyLength: KEY_LENGTH, +if (HOST) config.host = HOST; +if (PORT) config.port = PORT; - maxLength: MAX_LENGTH, +if (KEY_LENGTH) config.keyLength = KEY_LENGTH; - staticMaxAge: STATIC_MAX_AGE, +if (MAX_LENGTH) config.maxLength = MAX_LENGTH; - recompressStaticAssets: RECOMPRESS_STATIC_ASSETS, +if (PORT) config.staticMaxAge = STATIC_MAX_AGE; - logging: [ - { - level: LOGGING_LEVEL, - type: LOGGING_TYPE, - colorize: LOGGING_COLORIZE, - }, - ], +if (PORT) config.ecompressStaticAssets = RECOMPRESS_STATIC_ASSETS; - keyGenerator: { - type: KEYGENERATOR_TYPE, - keyspace: KEY_GENERATOR_KEYSPACE, - }, +if (LOGGING_LEVEL) config.logging[0].level = LOGGING_LEVEL; +if (LOGGING_TYPE) config.logging[0].type = LOGGING_TYPE; +if (LOGGING_COLORIZE) config.logging[0].colorize = LOGGING_COLORIZE; - rateLimits: { - whitelist: RATE_LIMITS_WHITELIST ? RATE_LIMITS_WHITELIST.split(",") : [], - blacklist: RATE_LIMITS_BLACKLIST ? RATE_LIMITS_BLACKLIST.split(",") : [], - categories: { - normal: { - totalRequests: RATE_LIMITS_NORMAL_TOTAL_REQUESTS, - every: RATE_LIMITS_NORMAL_EVERY_MILLISECONDS, - }, - whitelist: - RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS || - RATE_LIMITS_WHITELIST_TOTAL_REQUESTS - ? { - totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS, - every: RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS, - } - : null, - blacklist: - RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS || - RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS - ? { - totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS, - every: RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS, - } - : null, - }, - }, +if (KEYGENERATOR_TYPE) config.keyGenerator.type = KEYGENERATOR_TYPE; +if (KEY_GENERATOR_KEYSPACE) config.keyGenerator.keyspace = KEY_GENERATOR_KEYSPACE; - storage: { - type: STORAGE_TYPE, - host: STORAGE_HOST, - port: STORAGE_PORT, - expire: STORAGE_EXPIRE_SECONDS, - bucket: STORAGE_AWS_BUCKET, - region: STORAGE_AWS_REGION, - connectionUrl: `postgres://${STORAGE_USERNAME}:${STORAGE_PASSWORD}@${STORAGE_HOST}:${STORAGE_PORT}/${STORAGE_DB}`, - db: STORAGE_DB, - user: STORAGE_USERNAME, - password: STORAGE_PASSWORD, - path: STORAGE_FILEPATH, - }, +if (RATE_LIMITS_WHITELIST) config.rateLimits.whitelist = RATE_LIMITS_WHITELIST.split(","); +if (RATE_LIMITS_BLACKLIST) config.rateLimits.blacklist = RATE_LIMITS_BLACKLIST.split(","); - documents: DOCUMENTS - ? DOCUMENTS.split(",").reduce((acc, item) => { - const keyAndValueArray = item.replace(/\s/g, "").split("="); - return { ...acc, [keyAndValueArray[0]]: keyAndValueArray[1] }; - }, {}) - : null, -}; +if (RATE_LIMITS_NORMAL_TOTAL_REQUESTS) config.rateLimits.categories.normal.totalRequests = RATE_LIMITS_NORMAL_TOTAL_REQUESTS; +if (RATE_LIMITS_NORMAL_EVERY_MILLISECONDS) config.rateLimits.categories.normal.every = RATE_LIMITS_NORMAL_EVERY_MILLISECONDS; + +if (RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS || RATE_LIMITS_WHITELIST_TOTAL_REQUESTS) config.rateLimits.categories.whitelist = { totalRequests: RATE_LIMITS_WHITELIST_TOTAL_REQUESTS, every: RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS }; +if (RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS || RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS) config.rateLimits.categories.blacklist = { totalRequests: RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS, every: RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS }; + +if (STORAGE_TYPE) config.storage.type = STORAGE_TYPE; +if (STORAGE_HOST) config.storage.host = STORAGE_HOST; +if (STORAGE_PORT) config.storage.port = STORAGE_PORT; +if (STORAGE_EXPIRE_SECONDS) config.storage.expire = STORAGE_EXPIRE_SECONDS; +if (STORAGE_AWS_BUCKET) config.storage.bucket = STORAGE_AWS_BUCKET; +if (STORAGE_AWS_REGION) config.storage.region = STORAGE_AWS_REGION; +if (STORAGE_DB) config.storage.connectionUrl = `postgres://${STORAGE_USERNAME}:${STORAGE_PASSWORD}@${STORAGE_HOST}:${STORAGE_PORT}/${STORAGE_DB}`; +if (STORAGE_DB) config.storage.db = STORAGE_DB; +if (STORAGE_USERNAME) config.storage.user = STORAGE_USERNAME; +if (STORAGE_PASSWORD) config.storage.password = STORAGE_PASSWORD; +if (STORAGE_FILEPATH) config.storage.path = STORAGE_FILEPATH; +if (STORAGE_ALLOWLIST) config.storage.allowList = STORAGE_ALLOWLIST == "true"; +if (STORAGE_ALLOWDELETE) config.storage.allowDelete = STORAGE_ALLOWDELETE == "true"; + +if (DOCUMENTS) { + config.documents = DOCUMENTS.split(",").reduce((acc, item) => { + const keyAndValueArray = item.replace(/\s/g, "").split("="); + return { ...acc, [keyAndValueArray[0]]: keyAndValueArray[1] }; + }, {}); +} console.log(JSON.stringify(config)); diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 0b089d8..03c1d1e 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -4,6 +4,6 @@ set -e -node ./docker-entrypoint.js > ./config.js +node ./docker-entrypoint.js > ./config.json exec "$@" diff --git a/lib/document_handler.js b/lib/document_handler.js index 14da9b2..9b33c63 100644 --- a/lib/document_handler.js +++ b/lib/document_handler.js @@ -3,6 +3,8 @@ var Busboy = require('busboy'); // For handling serving stored documents +const checkkeyregex = /^[a-zA-Z0-9-_.]+$/; + var DocumentHandler = function(options) { if (!options) { options = {}; @@ -17,9 +19,19 @@ DocumentHandler.defaultKeyLength = 10; // Handle retrieving a document DocumentHandler.prototype.handleGet = function(request, response, config) { - const key = request.params.id.split('.')[0]; + const key = request.params.id; const skipExpire = !!config.documents[key]; + if ((key)&&(!checkkeyregex.test(key))) { + winston.warn('invalid key', { key: key }); + response.writeHead(400, { 'content-type': 'application/json' }); + response.end( + JSON.stringify({ message: 'Invalid key', key: key }) + ); + + return; + } + this.store.get(key, function(ret) { if (ret) { winston.verbose('retrieved document', { key: key }); @@ -44,9 +56,19 @@ DocumentHandler.prototype.handleGet = function(request, response, config) { // Handle retrieving the raw version of a document DocumentHandler.prototype.handleRawGet = function(request, response, config) { - const key = request.params.id.split('.')[0]; + const key = request.params.id; const skipExpire = !!config.documents[key]; + if ((key)&&(!checkkeyregex.test(key))) { + winston.warn('invalid key', { key: key }); + response.writeHead(400, { 'content-type': 'application/json' }); + response.end( + JSON.stringify({ message: 'Invalid key', key: key }) + ); + + return; + } + this.store.get(key, function(ret) { if (ret) { winston.verbose('retrieved raw document', { key: key }); @@ -75,6 +97,17 @@ DocumentHandler.prototype.handlePost = function (request, response) { var buffer = ''; var cancelled = false; + const key = request.params.id ? request.params.id : null; + if ((key)&&(!checkkeyregex.test(key))) { + winston.warn('invalid key', { key: key }); + response.writeHead(400, { 'content-type': 'application/json' }); + response.end( + JSON.stringify({ message: 'Invalid key', key: key }) + ); + + return; + } + // What to do when done var onSuccess = function () { // Check length @@ -87,9 +120,9 @@ DocumentHandler.prototype.handlePost = function (request, response) { ); return; } - // And then save if we should - _this.chooseKey(function (key) { - _this.store.set(key, buffer, function (res) { + + const store = function (key) { + _this.store.set(key, buffer, function (res) { if (res) { winston.verbose('added document', { key: key }); response.writeHead(200, { 'content-type': 'application/json' }); @@ -101,7 +134,16 @@ DocumentHandler.prototype.handlePost = function (request, response) { response.end(JSON.stringify({ message: 'Error adding document.' })); } }); - }); + } + + // And then save if we should + if (!key) { + _this.chooseKey(function (key) { + store(key); + }); + } else { + store(key); + } }; // If we should, parse a form to grab the data @@ -135,6 +177,69 @@ DocumentHandler.prototype.handlePost = function (request, response) { } }; + +// Handle deleting a document +DocumentHandler.prototype.handleDelete = function(request, response, config) { + const key = request.params.id; + const allowdelete = config.storage.allowDelete; + + if ((key)&&(!checkkeyregex.test(key))) { + winston.warn('invalid key', { key: key }); + response.writeHead(400, { 'content-type': 'application/json' }); + response.end( + JSON.stringify({ message: 'Invalid key', key: key }) + ); + + return; + } + + if (!this.store.delete||!allowdelete) { + winston.warn('document provider does not support delete', { key: key }); + response.writeHead(405, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ message: 'Delete not supported.' })); + return; + } + + this.store.delete(key, function(ret) { + if (ret) { + winston.verbose('deleted document', { key: key }); + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ message: 'Document deleted', key: key })); + } + else { + winston.warn('raw document not found', { key: key }); + response.writeHead(404, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ message: 'Document not found.' })); + } + }); +}; + +// Handle listing documents +DocumentHandler.prototype.handleList = function(request, response, config) { + const allowlist = config.storage.allowList; + + if (!this.store.list||!allowlist) { + winston.warn('document provider does not support list'); + response.writeHead(405, { 'content-type': 'application/json' }); + response.end(JSON.stringify({ message: 'Delete not supported.' })); + return; + } + + this.store.list(function(ret) { + winston.verbose('list documents'); + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify(ret)); + }); +}; + +// Handle listing documents +DocumentHandler.prototype.handleGetKey = function(request, response, config) { + this.chooseKey(function(ret) { + response.writeHead(200, { 'content-type': 'text/plain' }); + response.end(ret); + }); +}; + // Keep choosing keys until one isn't taken DocumentHandler.prototype.chooseKey = function(callback) { var key = this.acceptableKey(); diff --git a/lib/document_stores/file.js b/lib/document_stores/file.js index 7fd5995..0f10e6a 100644 --- a/lib/document_stores/file.js +++ b/lib/document_stores/file.js @@ -25,7 +25,7 @@ FileDocumentStore.prototype.set = function(key, data, callback, skipExpire) { try { var _this = this; fs.mkdir(this.basePath, '700', function() { - var fn = _this.basePath + '/' + FileDocumentStore.md5(key); + var fn = _this.basePath + '/' + key; fs.writeFile(fn, data, 'utf8', function(err) { if (err) { callback(false); @@ -46,7 +46,7 @@ FileDocumentStore.prototype.set = function(key, data, callback, skipExpire) { // Get data from a file from key FileDocumentStore.prototype.get = function(key, callback, skipExpire) { var _this = this; - var fn = this.basePath + '/' + FileDocumentStore.md5(key); + var fn = this.basePath + '/' + key; fs.readFile(fn, 'utf8', function(err, data) { if (err) { callback(false); @@ -60,4 +60,32 @@ FileDocumentStore.prototype.get = function(key, callback, skipExpire) { }); }; +// delete a file from key +FileDocumentStore.prototype.delete = function(key, callback) { + var _this = this; + var fn = this.basePath + '/' + key; + fs.unlink(fn, function(err) { + if (err) { + callback(false); + } + else { + callback(true); + } + }); +}; + +// list files +FileDocumentStore.prototype.list = function(callback) { + var _this = this; + var fn = this.basePath + '/'; + fs.readdir(fn, function(err, listfiles) { + if (err) { + callback(false); + } + else { + callback(listfiles); + } + }); +}; + module.exports = FileDocumentStore; diff --git a/package.json b/package.json index 453ae2c..2d13a86 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,17 @@ { - "name": "haste", - "version": "0.1.0", + "name": "hastepad", + "version": "0.1.1", "private": true, - "description": "Private Pastebin Server", + "description": "Private OnlineNotepad Server", "keywords": [ - "paste", - "pastebin" + "Notepad" ], "author": { "name": "John Crepezzi", "email": "john.crepezzi@gmail.com", "url": "http://seejohncode.com/" }, - "main": "haste", + "main": "hastepad", "dependencies": { "busboy": "0.2.4", "connect": "^3.7.0", diff --git a/server.js b/server.js index 0837a03..887e7e3 100644 --- a/server.js +++ b/server.js @@ -11,7 +11,7 @@ var connect_rate_limit = require('connect-ratelimit'); var DocumentHandler = require('./lib/document_handler'); // Load the configuration and set some defaults -const configPath = process.argv.length <= 2 ? 'config.js' : process.argv[2]; +const configPath = process.argv.length <= 2 ? 'config.json' : process.argv[2]; const config = JSON.parse(fs.readFileSync(configPath, 'utf8')); config.port = process.env.PORT || config.port || 7777; config.host = process.env.HOST || config.host || 'localhost'; @@ -125,14 +125,35 @@ app.use(route(function(router) { return documentHandler.handlePost(request, response); }); + // update documents + + router.post('/documents/:id', function(request, response) { + return documentHandler.handlePost(request, response); + }); + // get documents router.get('/documents/:id', function(request, response) { return documentHandler.handleGet(request, response, config); }); + // get documents + router.delete('/documents/:id', function(request, response) { + return documentHandler.handleDelete(request, response, config); + }); + router.head('/documents/:id', function(request, response) { return documentHandler.handleGet(request, response, config); }); + + // list documents + router.get('/documents', function(request, response) { + return documentHandler.handleList(request, response, config); + }); + + // get key + router.get('/key', function(request, response) { + return documentHandler.handleGetKey(request, response, config); + }); })); // Otherwise, try to match static files diff --git a/static/application.js b/static/application.js index 5f99ada..1959d7c 100644 --- a/static/application.js +++ b/static/application.js @@ -54,19 +54,18 @@ haste_document.prototype.load = function(key, callback, lang) { }; // Save this document to the server and lock it here -haste_document.prototype.save = function(data, callback) { +haste_document.prototype.save = function(key, data, callback) { if (this.locked) { return false; } this.data = data; var _this = this; - $.ajax('/documents', { + $.ajax('/documents/' + key, { type: 'post', data: data, dataType: 'json', contentType: 'text/plain; charset=utf-8', success: function(res) { - _this.locked = true; _this.key = res.key; var high = hljs.highlightAuto(data); callback(null, { @@ -87,6 +86,24 @@ haste_document.prototype.save = function(data, callback) { }); }; +// get a valid key from server +haste_document.prototype.getkey = function(callback) { + $.ajax('/key/', { + type: 'get', + success: function(res) { + callback(null, res); + }, + error: function(res) { + try { + callback($.parseJSON(res.responseText)); + } + catch (e) { + callback({message: 'Something went wrong!'}); + } + } + }); +}; + ///// represents the paste application var haste = function(appName, options) { @@ -129,6 +146,11 @@ haste.prototype.fullKey = function() { this.configureKey(['new', 'duplicate', 'twitter', 'raw']); }; +// Show all the keys +haste.prototype.allKey = function() { + this.configureKey(['new', 'save', 'duplicate', 'twitter', 'raw']); +}; + // Set the key up for certain things to be enabled haste.prototype.configureKey = function(enable) { var $this, i = 0; @@ -144,20 +166,60 @@ haste.prototype.configureKey = function(enable) { }); }; +haste.prototype.getCurrentKey = function () { + var key = window.location.pathname; + if (key == "/") + key = null; + else + key = key.substr(1); + + return key; +} + + +// Get this document from the server and lock it here +haste.prototype.getList = function(callback) { + var _this = this; + $.ajax('/documents/', { + type: 'get', + dataType: 'json', + success: function(res) { + callback(res); + }, + error: function() { + callback(false); + } + }); +}; + // Remove the current document (if there is one) // and set up for a new one -haste.prototype.newDocument = function(hideHistory) { - this.$box.hide(); - this.doc = new haste_document(); - if (!hideHistory) { - window.history.pushState(null, this.appName, '/'); +haste.prototype.newDocument = function(forcenewkey, callback) { + var _this = this; + + var key = this.getCurrentKey(); + var newdoc = function(key) { + window.history.pushState(null, _this.appName, '/'+key); + _this.$box.hide(); + _this.doc = new haste_document(); + _this.setTitle(); + _this.lightKey(); + _this.$textarea.val('').show('fast', function() { + this.focus(); + }); + _this.removeLineNumbers(); + if (callback) { + callback(_this.doc); + } } - this.setTitle(); - this.lightKey(); - this.$textarea.val('').show('fast', function() { - this.focus(); - }); - this.removeLineNumbers(); + if (key&&!forcenewkey) { + newdoc(key); + } else { + haste_document.prototype.getkey(function (err, key) { + newdoc(key); + }); + } + _this.updateList(); }; // Map of common extensions @@ -210,7 +272,7 @@ haste.prototype.loadDocument = function(key) { // Ask for what we want var _this = this; _this.doc = new haste_document(); - _this.doc.load(parts[0], function(ret) { + _this.doc.load(key, function(ret) { if (ret) { _this.$code.html(ret.value); _this.setTitle(ret.key); @@ -223,32 +285,31 @@ haste.prototype.loadDocument = function(key) { _this.newDocument(); } }, this.lookupTypeByExtension(parts[1])); + this.updateList(); }; // Duplicate the current document - only if locked haste.prototype.duplicateDocument = function() { - if (this.doc.locked) { - var currentData = this.doc.data; - this.newDocument(); - this.$textarea.val(currentData); + var _this = this; + if (_this.doc.locked) { + var currentData = _this.doc.data; + _this.newDocument(true, function () { + _this.$textarea.val(currentData); + }); } }; // Lock the current document haste.prototype.lockDocument = function() { var _this = this; - this.doc.save(this.$textarea.val(), function(err, ret) { + this.doc.save(this.getCurrentKey(), this.$textarea.val(), function(err, ret) { if (err) { _this.showMessage(err.message, 'error'); } else if (ret) { + _this.doc.locked = true; _this.$code.html(ret.value); _this.setTitle(ret.key); - var file = '/' + ret.key; - if (ret.language) { - file += '.' + _this.lookupExtensionByType(ret.language); - } - window.history.pushState(null, _this.appName + '-' + ret.key, file); _this.fullKey(); _this.$textarea.val('').hide(); _this.$box.show().focus(); @@ -257,6 +318,16 @@ haste.prototype.lockDocument = function() { }); }; +// UnLock the current document +haste.prototype.unlockDocument = function() { + var _this = this; + _this.$textarea.val(_this.$code.text()).show().focus(); + _this.$box.hide(); + _this.allKey(); + _this.removeLineNumbers(); + _this.doc.locked = false; +}; + haste.prototype.configureButtons = function() { var _this = this; this.buttons = [ @@ -281,7 +352,7 @@ haste.prototype.configureButtons = function() { }, shortcutDescription: 'control + n', action: function() { - _this.newDocument(!_this.doc.key); + _this.newDocument(true); } }, { @@ -308,13 +379,14 @@ haste.prototype.configureButtons = function() { }, { $where: $('#box2 .twitter'), - label: 'Twitter', + label: 'Edit', shortcut: function(evt) { return _this.options.twitter && _this.doc.locked && evt.shiftKey && evt.ctrlKey && evt.keyCode == 84; }, shortcutDescription: 'control + shift + t', action: function() { - window.open('https://twitter.com/share?url=' + encodeURI(window.location.href)); + //window.open('https://twitter.com/share?url=' + encodeURI(window.location.href)); + _this.unlockDocument(); } } ]; @@ -345,6 +417,19 @@ haste.prototype.configureButton = function(options) { }); }; +haste.prototype.updateList = function () { + var _this = this; + _this.getList(function (lst) { + if (lst) { + var lis = ""; + lst.forEach(function (file) { + lis+= '
  • '+file+'
  • '; + }); + $('#list').html(lis); + } + }); +}; + // Configure keyboard shortcuts for the textarea haste.prototype.configureShortcuts = function() { var _this = this; @@ -361,6 +446,27 @@ haste.prototype.configureShortcuts = function() { }); }; +haste.prototype.autosave = function() { + var _this = this; + _this.$textarea.on('keydown', function () { + _this.doc.changed = true; + }); + + var cycle = function () { + if ((_this.doc)&&(!_this.doc.locked)&&(_this.doc.changed)) { + _this.doc.save(_this.getCurrentKey(), _this.$textarea.val(), function (err, data) { + if (err) { + _this.showMessage("Error "+err); + } + }); + _this.doc.changed = false; + } + window.setTimeout(cycle, 15000); + }; + + window.setTimeout(cycle, 5000); +}; + ///// Tab behavior in the textarea - 2 spaces per tab $(function() { diff --git a/static/application.min.js b/static/application.min.js index 512d158..4119164 100644 --- a/static/application.min.js +++ b/static/application.min.js @@ -1 +1 @@ -var haste_document=function(){this.locked=!1};haste_document.prototype.htmlEscape=function(t){return t.replace(/&/g,"&").replace(/>/g,">").replace(/'+t+"");$("#messages").prepend(o),setTimeout(function(){o.slideUp("fast",function(){$(this).remove()})},3e3)},haste.prototype.lightKey=function(){this.configureKey(["new","save"])},haste.prototype.fullKey=function(){this.configureKey(["new","duplicate","twitter","raw"])},haste.prototype.configureKey=function(t){var e,o=0;$("#box2 .function").each(function(){for(e=$(this),o=0;o";$("#linenos").html(e)},haste.prototype.removeLineNumbers=function(){$("#linenos").html(">")},haste.prototype.loadDocument=function(t){var e=t.split(".",2),o=this;o.doc=new haste_document,o.doc.load(e[0],function(t){t?(o.$code.html(t.value),o.setTitle(t.key),o.fullKey(),o.$textarea.val("").hide(),o.$box.show().focus(),o.addLineNumbers(t.lineCount)):o.newDocument()},this.lookupTypeByExtension(e[1]))},haste.prototype.duplicateDocument=function(){if(this.doc.locked){var t=this.doc.data;this.newDocument(),this.$textarea.val(t)}},haste.prototype.lockDocument=function(){var t=this;this.doc.save(this.$textarea.val(),function(e,o){if(e)t.showMessage(e.message,"error");else if(o){t.$code.html(o.value),t.setTitle(o.key);var n="/"+o.key;o.language&&(n+="."+t.lookupExtensionByType(o.language)),window.history.pushState(null,t.appName+"-"+o.key,n),t.fullKey(),t.$textarea.val("").hide(),t.$box.show().focus(),t.addLineNumbers(o.lineCount)}})},haste.prototype.configureButtons=function(){var t=this;this.buttons=[{$where:$("#box2 .save"),label:"Save",shortcutDescription:"control + s",shortcut:function(t){return t.ctrlKey&&83===t.keyCode},action:function(){""!==t.$textarea.val().replace(/^\s+|\s+$/g,"")&&t.lockDocument()}},{$where:$("#box2 .new"),label:"New",shortcut:function(t){return t.ctrlKey&&78===t.keyCode},shortcutDescription:"control + n",action:function(){t.newDocument(!t.doc.key)}},{$where:$("#box2 .duplicate"),label:"Duplicate & Edit",shortcut:function(e){return t.doc.locked&&e.ctrlKey&&68===e.keyCode},shortcutDescription:"control + d",action:function(){t.duplicateDocument()}},{$where:$("#box2 .raw"),label:"Just Text",shortcut:function(t){return t.ctrlKey&&t.shiftKey&&82===t.keyCode},shortcutDescription:"control + shift + r",action:function(){window.location.href="/raw/"+t.doc.key}},{$where:$("#box2 .twitter"),label:"Twitter",shortcut:function(e){return t.options.twitter&&t.doc.locked&&e.shiftKey&&e.ctrlKey&&84==e.keyCode},shortcutDescription:"control + shift + t",action:function(){window.open("https://twitter.com/share?url="+encodeURI(window.location.href))}}];for(var e=0;e/g,">").replace(/'+t+"");$("#messages").prepend(o),setTimeout(function(){o.slideUp("fast",function(){$(this).remove()})},3e3)},haste.prototype.lightKey=function(){this.configureKey(["new","save"])},haste.prototype.fullKey=function(){this.configureKey(["new","duplicate","twitter","raw"])},haste.prototype.allKey=function(){this.configureKey(["new","save","duplicate","twitter","raw"])},haste.prototype.configureKey=function(t){var e,o=0;$("#box2 .function").each(function(){for(e=$(this),o=0;o";$("#linenos").html(e)},haste.prototype.removeLineNumbers=function(){$("#linenos").html(">")},haste.prototype.loadDocument=function(t){var e=t.split(".",2),o=this;o.doc=new haste_document,o.doc.load(t,function(t){t?(o.$code.html(t.value),o.setTitle(t.key),o.fullKey(),o.$textarea.val("").hide(),o.$box.show().focus(),o.addLineNumbers(t.lineCount)):o.newDocument()},this.lookupTypeByExtension(e[1])),this.updateList()},haste.prototype.duplicateDocument=function(){var t=this;if(t.doc.locked){var e=t.doc.data;t.newDocument(!0,function(){t.$textarea.val(e)})}},haste.prototype.lockDocument=function(){var t=this;this.doc.save(this.getCurrentKey(),this.$textarea.val(),function(e,o){e?t.showMessage(e.message,"error"):o&&(t.doc.locked=!0,t.$code.html(o.value),t.setTitle(o.key),t.fullKey(),t.$textarea.val("").hide(),t.$box.show().focus(),t.addLineNumbers(o.lineCount))})},haste.prototype.unlockDocument=function(){var t=this;t.$textarea.val(t.$code.text()).show().focus(),t.$box.hide(),t.allKey(),t.removeLineNumbers(),t.doc.locked=!1},haste.prototype.configureButtons=function(){var t=this;this.buttons=[{$where:$("#box2 .save"),label:"Save",shortcutDescription:"control + s",shortcut:function(t){return t.ctrlKey&&83===t.keyCode},action:function(){""!==t.$textarea.val().replace(/^\s+|\s+$/g,"")&&t.lockDocument()}},{$where:$("#box2 .new"),label:"New",shortcut:function(t){return t.ctrlKey&&78===t.keyCode},shortcutDescription:"control + n",action:function(){t.newDocument(!0)}},{$where:$("#box2 .duplicate"),label:"Duplicate & Edit",shortcut:function(e){return t.doc.locked&&e.ctrlKey&&68===e.keyCode},shortcutDescription:"control + d",action:function(){t.duplicateDocument()}},{$where:$("#box2 .raw"),label:"Just Text",shortcut:function(t){return t.ctrlKey&&t.shiftKey&&82===t.keyCode},shortcutDescription:"control + shift + r",action:function(){window.location.href="/raw/"+t.doc.key}},{$where:$("#box2 .twitter"),label:"Edit",shortcut:function(e){return t.options.twitter&&t.doc.locked&&e.shiftKey&&e.ctrlKey&&84==e.keyCode},shortcutDescription:"control + shift + t",action:function(){t.unlockDocument()}}];for(var e=0;e'+t+""}),$("#list").html(e)}})},haste.prototype.configureShortcuts=function(){var t=this;$(document.body).keydown(function(e){for(var o,n=0;n @@ -51,12 +52,15 @@ - + +
    +
      +