Compare commits
No commits in common. 'master' and 'raw_icon' have entirely different histories.
44 changed files with 435 additions and 3255 deletions
@ -1,8 +0,0 @@ |
|||||||
Dockerfile |
|
||||||
.git |
|
||||||
npm-debug.log |
|
||||||
node_modules |
|
||||||
*.swp |
|
||||||
*.swo |
|
||||||
data |
|
||||||
*.DS_Store |
|
@ -1,2 +0,0 @@ |
|||||||
**/*.min.js |
|
||||||
config.js |
|
@ -1,25 +0,0 @@ |
|||||||
{ |
|
||||||
"env": { |
|
||||||
"es6": true, |
|
||||||
"node": true |
|
||||||
}, |
|
||||||
"extends": "eslint:recommended", |
|
||||||
"rules": { |
|
||||||
"indent": [ |
|
||||||
"error", |
|
||||||
2 |
|
||||||
], |
|
||||||
"linebreak-style": [ |
|
||||||
"error", |
|
||||||
"unix" |
|
||||||
], |
|
||||||
"quotes": [ |
|
||||||
"error", |
|
||||||
"single" |
|
||||||
], |
|
||||||
"semi": [ |
|
||||||
"error", |
|
||||||
"always" |
|
||||||
] |
|
||||||
} |
|
||||||
} |
|
@ -1 +0,0 @@ |
|||||||
* @toptal/site-acquisition-eng |
|
@ -1,30 +0,0 @@ |
|||||||
name: Close inactive issues and PRs |
|
||||||
on: |
|
||||||
workflow_dispatch: |
|
||||||
schedule: |
|
||||||
- cron: "30 1 * * *" |
|
||||||
|
|
||||||
jobs: |
|
||||||
close-stale: |
|
||||||
runs-on: ubuntu-latest |
|
||||||
permissions: |
|
||||||
issues: write |
|
||||||
pull-requests: write |
|
||||||
steps: |
|
||||||
- uses: actions/stale@v3 |
|
||||||
with: |
|
||||||
days-before-stale: 30 |
|
||||||
days-before-close: 14 |
|
||||||
stale-issue-label: "stale" |
|
||||||
stale-pr-label: "stale" |
|
||||||
|
|
||||||
exempt-issue-labels: backlog,triage,nostale |
|
||||||
exempt-pr-labels: backlog,triage,nostale |
|
||||||
|
|
||||||
stale-pr-message: "This PR is stale because it has been open for 30 days with no activity." |
|
||||||
close-pr-message: "This PR was closed because it has been inactive for 14 days since being marked as stale." |
|
||||||
|
|
||||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity." |
|
||||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale." |
|
||||||
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }} |
|
@ -1,68 +0,0 @@ |
|||||||
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 . . |
|
||||||
|
|
||||||
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 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_USENAME= \ |
|
||||||
STORAGE_PASSWORD= \ |
|
||||||
STORAGE_FILEPATH= |
|
||||||
|
|
||||||
ENV LOGGING_LEVEL=verbose \ |
|
||||||
LOGGING_TYPE=Console \ |
|
||||||
LOGGING_COLORIZE=true |
|
||||||
|
|
||||||
ENV HOST=0.0.0.0\ |
|
||||||
PORT=7777\ |
|
||||||
KEY_LENGTH=10\ |
|
||||||
MAX_LENGTH=400000\ |
|
||||||
STATIC_MAX_AGE=86400\ |
|
||||||
RECOMPRESS_STATIC_ASSETS=true |
|
||||||
|
|
||||||
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 |
|
||||||
|
|
||||||
EXPOSE ${PORT} |
|
||||||
STOPSIGNAL SIGINT |
|
||||||
ENTRYPOINT [ "bash", "docker-entrypoint.sh" ] |
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s \ |
|
||||||
--retries=3 CMD [ "sh", "-c", "echo -n 'curl localhost:7777... '; \ |
|
||||||
(\ |
|
||||||
curl -sf localhost:7777 > /dev/null\ |
|
||||||
) && echo OK || (\ |
|
||||||
echo Fail && exit 2\ |
|
||||||
)"] |
|
||||||
CMD ["npm", "start"] |
|
@ -1,12 +0,0 @@ |
|||||||
version: '3.0' |
|
||||||
services: |
|
||||||
haste-server: |
|
||||||
build: . |
|
||||||
environment: |
|
||||||
- STORAGE_TYPE=memcached |
|
||||||
- STORAGE_HOST=memcached |
|
||||||
- STORAGE_PORT=11211 |
|
||||||
ports: |
|
||||||
- 7777:7777 |
|
||||||
memcached: |
|
||||||
image: memcached:latest |
|
@ -1,108 +0,0 @@ |
|||||||
const { |
|
||||||
HOST, |
|
||||||
PORT, |
|
||||||
KEY_LENGTH, |
|
||||||
MAX_LENGTH, |
|
||||||
STATIC_MAX_AGE, |
|
||||||
RECOMPRESS_STATIC_ASSETS, |
|
||||||
STORAGE_TYPE, |
|
||||||
STORAGE_HOST, |
|
||||||
STORAGE_PORT, |
|
||||||
STORAGE_EXPIRE_SECONDS, |
|
||||||
STORAGE_DB, |
|
||||||
STORAGE_AWS_BUCKET, |
|
||||||
STORAGE_AWS_REGION, |
|
||||||
STORAGE_PASSWORD, |
|
||||||
STORAGE_USERNAME, |
|
||||||
STORAGE_FILEPATH, |
|
||||||
LOGGING_LEVEL, |
|
||||||
LOGGING_TYPE, |
|
||||||
LOGGING_COLORIZE, |
|
||||||
KEYGENERATOR_TYPE, |
|
||||||
KEY_GENERATOR_KEYSPACE, |
|
||||||
RATE_LIMITS_NORMAL_TOTAL_REQUESTS, |
|
||||||
RATE_LIMITS_NORMAL_EVERY_MILLISECONDS, |
|
||||||
RATE_LIMITS_WHITELIST_TOTAL_REQUESTS, |
|
||||||
RATE_LIMITS_WHITELIST_EVERY_MILLISECONDS, |
|
||||||
RATE_LIMITS_WHITELIST, |
|
||||||
RATE_LIMITS_BLACKLIST_TOTAL_REQUESTS, |
|
||||||
RATE_LIMITS_BLACKLIST_EVERY_MILLISECONDS, |
|
||||||
RATE_LIMITS_BLACKLIST, |
|
||||||
DOCUMENTS, |
|
||||||
} = process.env; |
|
||||||
|
|
||||||
const config = { |
|
||||||
host: HOST, |
|
||||||
port: Number(PORT), |
|
||||||
|
|
||||||
keyLength: Number(KEY_LENGTH), |
|
||||||
|
|
||||||
maxLength: Number(MAX_LENGTH), |
|
||||||
|
|
||||||
staticMaxAge: Number(STATIC_MAX_AGE), |
|
||||||
|
|
||||||
recompressStaticAssets: RECOMPRESS_STATIC_ASSETS, |
|
||||||
|
|
||||||
logging: [ |
|
||||||
{ |
|
||||||
level: LOGGING_LEVEL, |
|
||||||
type: LOGGING_TYPE, |
|
||||||
colorize: LOGGING_COLORIZE, |
|
||||||
}, |
|
||||||
], |
|
||||||
|
|
||||||
keyGenerator: { |
|
||||||
type: KEYGENERATOR_TYPE, |
|
||||||
keyspace: KEY_GENERATOR_KEYSPACE, |
|
||||||
}, |
|
||||||
|
|
||||||
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, |
|
||||||
}, |
|
||||||
}, |
|
||||||
|
|
||||||
storage: { |
|
||||||
type: STORAGE_TYPE, |
|
||||||
host: STORAGE_HOST, |
|
||||||
port: Number(STORAGE_PORT), |
|
||||||
expire: Number(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, |
|
||||||
}, |
|
||||||
|
|
||||||
documents: DOCUMENTS |
|
||||||
? DOCUMENTS.split(",").reduce((acc, item) => { |
|
||||||
const keyAndValueArray = item.replace(/\s/g, "").split("="); |
|
||||||
return { ...acc, [keyAndValueArray[0]]: keyAndValueArray[1] }; |
|
||||||
}, {}) |
|
||||||
: null, |
|
||||||
}; |
|
||||||
|
|
||||||
console.log(JSON.stringify(config)); |
|
@ -1,9 +0,0 @@ |
|||||||
#!/bin/bash |
|
||||||
|
|
||||||
# We use this file to translate environmental variables to .env files used by the application |
|
||||||
|
|
||||||
set -e |
|
||||||
|
|
||||||
node ./docker-entrypoint.js > ./config.js |
|
||||||
|
|
||||||
exec "$@" |
|
@ -1,56 +0,0 @@ |
|||||||
/*global require,module,process*/ |
|
||||||
|
|
||||||
var AWS = require('aws-sdk'); |
|
||||||
var winston = require('winston'); |
|
||||||
|
|
||||||
var AmazonS3DocumentStore = function(options) { |
|
||||||
this.expire = options.expire; |
|
||||||
this.bucket = options.bucket; |
|
||||||
this.client = new AWS.S3({region: options.region}); |
|
||||||
}; |
|
||||||
|
|
||||||
AmazonS3DocumentStore.prototype.get = function(key, callback, skipExpire) { |
|
||||||
var _this = this; |
|
||||||
|
|
||||||
var req = { |
|
||||||
Bucket: _this.bucket, |
|
||||||
Key: key |
|
||||||
}; |
|
||||||
|
|
||||||
_this.client.getObject(req, function(err, data) { |
|
||||||
if(err) { |
|
||||||
callback(false); |
|
||||||
} |
|
||||||
else { |
|
||||||
callback(data.Body.toString('utf-8')); |
|
||||||
if (_this.expire && !skipExpire) { |
|
||||||
winston.warn('amazon s3 store cannot set expirations on keys'); |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
AmazonS3DocumentStore.prototype.set = function(key, data, callback, skipExpire) { |
|
||||||
var _this = this; |
|
||||||
|
|
||||||
var req = { |
|
||||||
Bucket: _this.bucket, |
|
||||||
Key: key, |
|
||||||
Body: data, |
|
||||||
ContentType: 'text/plain' |
|
||||||
}; |
|
||||||
|
|
||||||
_this.client.putObject(req, function(err, data) { |
|
||||||
if (err) { |
|
||||||
callback(false); |
|
||||||
} |
|
||||||
else { |
|
||||||
callback(true); |
|
||||||
if (_this.expire && !skipExpire) { |
|
||||||
winston.warn('amazon s3 store cannot set expirations on keys'); |
|
||||||
} |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = AmazonS3DocumentStore; |
|
@ -1,89 +0,0 @@ |
|||||||
/*global require,module,process*/ |
|
||||||
|
|
||||||
const Datastore = require('@google-cloud/datastore'); |
|
||||||
const winston = require('winston'); |
|
||||||
|
|
||||||
class GoogleDatastoreDocumentStore { |
|
||||||
|
|
||||||
// Create a new store with options
|
|
||||||
constructor(options) { |
|
||||||
this.kind = "Haste"; |
|
||||||
this.expire = options.expire; |
|
||||||
this.datastore = new Datastore(); |
|
||||||
} |
|
||||||
|
|
||||||
// Save file in a key
|
|
||||||
set(key, data, callback, skipExpire) { |
|
||||||
var expireTime = (skipExpire || this.expire === undefined) ? null : new Date(Date.now() + this.expire * 1000); |
|
||||||
|
|
||||||
var taskKey = this.datastore.key([this.kind, key]) |
|
||||||
var task = { |
|
||||||
key: taskKey, |
|
||||||
data: [ |
|
||||||
{ |
|
||||||
name: 'value', |
|
||||||
value: data, |
|
||||||
excludeFromIndexes: true |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: 'expiration', |
|
||||||
value: expireTime |
|
||||||
} |
|
||||||
] |
|
||||||
}; |
|
||||||
|
|
||||||
this.datastore.insert(task).then(() => { |
|
||||||
callback(true); |
|
||||||
}) |
|
||||||
.catch(err => { |
|
||||||
callback(false); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// Get a file from a key
|
|
||||||
get(key, callback, skipExpire) { |
|
||||||
var taskKey = this.datastore.key([this.kind, key]) |
|
||||||
|
|
||||||
this.datastore.get(taskKey).then((entity) => { |
|
||||||
if (skipExpire || entity[0]["expiration"] == null) { |
|
||||||
callback(entity[0]["value"]); |
|
||||||
} |
|
||||||
else { |
|
||||||
// check for expiry
|
|
||||||
if (entity[0]["expiration"] < new Date()) { |
|
||||||
winston.info("document expired", {key: key, expiration: entity[0]["expiration"], check: new Date(null)}); |
|
||||||
callback(false); |
|
||||||
} |
|
||||||
else { |
|
||||||
// update expiry
|
|
||||||
var task = { |
|
||||||
key: taskKey, |
|
||||||
data: [ |
|
||||||
{ |
|
||||||
name: 'value', |
|
||||||
value: entity[0]["value"], |
|
||||||
excludeFromIndexes: true |
|
||||||
}, |
|
||||||
{ |
|
||||||
name: 'expiration', |
|
||||||
value: new Date(Date.now() + this.expire * 1000) |
|
||||||
} |
|
||||||
] |
|
||||||
}; |
|
||||||
this.datastore.update(task).then(() => { |
|
||||||
}) |
|
||||||
.catch(err => { |
|
||||||
winston.error("failed to update expiration", {error: err}); |
|
||||||
}); |
|
||||||
callback(entity[0]["value"]); |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
.catch(err => { |
|
||||||
winston.error("Error retrieving value from Google Datastore", {error: err}); |
|
||||||
callback(false); |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = GoogleDatastoreDocumentStore; |
|
@ -1,54 +0,0 @@ |
|||||||
const memcached = require('memcached'); |
|
||||||
const winston = require('winston'); |
|
||||||
|
|
||||||
class MemcachedDocumentStore { |
|
||||||
|
|
||||||
// Create a new store with options
|
|
||||||
constructor(options) { |
|
||||||
this.expire = options.expire; |
|
||||||
|
|
||||||
const host = options.host || '127.0.0.1'; |
|
||||||
const port = options.port || 11211; |
|
||||||
const url = `${host}:${port}`; |
|
||||||
this.connect(url); |
|
||||||
} |
|
||||||
|
|
||||||
// Create a connection
|
|
||||||
connect(url) { |
|
||||||
this.client = new memcached(url); |
|
||||||
|
|
||||||
winston.info(`connecting to memcached on ${url}`); |
|
||||||
|
|
||||||
this.client.on('failure', function(error) { |
|
||||||
winston.info('error connecting to memcached', {error}); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// Save file in a key
|
|
||||||
set(key, data, callback, skipExpire) { |
|
||||||
this.client.set(key, data, skipExpire ? 0 : this.expire || 0, (error) => { |
|
||||||
callback(!error); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// Get a file from a key
|
|
||||||
get(key, callback, skipExpire) { |
|
||||||
this.client.get(key, (error, data) => { |
|
||||||
const value = error ? false : data; |
|
||||||
|
|
||||||
callback(value); |
|
||||||
|
|
||||||
// Update the key so that the expiration is pushed forward
|
|
||||||
if (value && !skipExpire) { |
|
||||||
this.set(key, data, (updateSucceeded) => { |
|
||||||
if (!updateSucceeded) { |
|
||||||
winston.error('failed to update expiration on GET', {key}); |
|
||||||
} |
|
||||||
}, skipExpire); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
module.exports = MemcachedDocumentStore; |
|
@ -1,88 +0,0 @@ |
|||||||
|
|
||||||
|
|
||||||
var MongoClient = require('mongodb').MongoClient, |
|
||||||
winston = require('winston'); |
|
||||||
|
|
||||||
var MongoDocumentStore = function (options) { |
|
||||||
this.expire = options.expire; |
|
||||||
this.connectionUrl = process.env.DATABASE_URl || options.connectionUrl; |
|
||||||
}; |
|
||||||
|
|
||||||
MongoDocumentStore.prototype.set = function (key, data, callback, skipExpire) { |
|
||||||
var now = Math.floor(new Date().getTime() / 1000), |
|
||||||
that = this; |
|
||||||
|
|
||||||
this.safeConnect(function (err, db) { |
|
||||||
if (err) |
|
||||||
return callback(false); |
|
||||||
|
|
||||||
db.collection('entries').update({ |
|
||||||
'entry_id': key, |
|
||||||
$or: [ |
|
||||||
{ expiration: -1 }, |
|
||||||
{ expiration: { $gt: now } } |
|
||||||
] |
|
||||||
}, { |
|
||||||
'entry_id': key, |
|
||||||
'value': data, |
|
||||||
'expiration': that.expire && !skipExpire ? that.expire + now : -1 |
|
||||||
}, { |
|
||||||
upsert: true |
|
||||||
}, function (err, existing) { |
|
||||||
if (err) { |
|
||||||
winston.error('error persisting value to mongodb', { error: err }); |
|
||||||
return callback(false); |
|
||||||
} |
|
||||||
|
|
||||||
callback(true); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
MongoDocumentStore.prototype.get = function (key, callback, skipExpire) { |
|
||||||
var now = Math.floor(new Date().getTime() / 1000), |
|
||||||
that = this; |
|
||||||
|
|
||||||
this.safeConnect(function (err, db) { |
|
||||||
if (err) |
|
||||||
return callback(false); |
|
||||||
|
|
||||||
db.collection('entries').findOne({ |
|
||||||
'entry_id': key, |
|
||||||
$or: [ |
|
||||||
{ expiration: -1 }, |
|
||||||
{ expiration: { $gt: now } } |
|
||||||
] |
|
||||||
}, function (err, entry) { |
|
||||||
if (err) { |
|
||||||
winston.error('error persisting value to mongodb', { error: err }); |
|
||||||
return callback(false); |
|
||||||
} |
|
||||||
|
|
||||||
callback(entry === null ? false : entry.value); |
|
||||||
|
|
||||||
if (entry !== null && entry.expiration !== -1 && that.expire && !skipExpire) { |
|
||||||
db.collection('entries').update({ |
|
||||||
'entry_id': key |
|
||||||
}, { |
|
||||||
$set: { |
|
||||||
'expiration': that.expire + now |
|
||||||
} |
|
||||||
}, function (err, result) { }); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
MongoDocumentStore.prototype.safeConnect = function (callback) { |
|
||||||
MongoClient.connect(this.connectionUrl, function (err, db) { |
|
||||||
if (err) { |
|
||||||
winston.error('error connecting to mongodb', { error: err }); |
|
||||||
callback(err); |
|
||||||
} else { |
|
||||||
callback(undefined, db); |
|
||||||
} |
|
||||||
}); |
|
||||||
}; |
|
||||||
|
|
||||||
module.exports = MongoDocumentStore; |
|
@ -1,80 +0,0 @@ |
|||||||
/*global require,module,process*/ |
|
||||||
|
|
||||||
var winston = require('winston'); |
|
||||||
const {Pool} = require('pg'); |
|
||||||
|
|
||||||
// create table entries (id serial primary key, key varchar(255) not null, value text not null, expiration int, unique(key));
|
|
||||||
|
|
||||||
// A postgres document store
|
|
||||||
var PostgresDocumentStore = function (options) { |
|
||||||
this.expireJS = parseInt(options.expire, 10); |
|
||||||
|
|
||||||
const connectionString = process.env.DATABASE_URL || options.connectionUrl; |
|
||||||
this.pool = new Pool({connectionString}); |
|
||||||
}; |
|
||||||
|
|
||||||
PostgresDocumentStore.prototype = { |
|
||||||
|
|
||||||
// Set a given key
|
|
||||||
set: function (key, data, callback, skipExpire) { |
|
||||||
var now = Math.floor(new Date().getTime() / 1000); |
|
||||||
var that = this; |
|
||||||
this.safeConnect(function (err, client, done) { |
|
||||||
if (err) { return callback(false); } |
|
||||||
client.query('INSERT INTO entries (key, value, expiration) VALUES ($1, $2, $3)', [ |
|
||||||
key, |
|
||||||
data, |
|
||||||
that.expireJS && !skipExpire ? that.expireJS + now : null |
|
||||||
], function (err) { |
|
||||||
if (err) { |
|
||||||
winston.error('error persisting value to postgres', { error: err }); |
|
||||||
return callback(false); |
|
||||||
} |
|
||||||
callback(true); |
|
||||||
done(); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}, |
|
||||||
|
|
||||||
// Get a given key's data
|
|
||||||
get: function (key, callback, skipExpire) { |
|
||||||
var now = Math.floor(new Date().getTime() / 1000); |
|
||||||
var that = this; |
|
||||||
this.safeConnect(function (err, client, done) { |
|
||||||
if (err) { return callback(false); } |
|
||||||
client.query('SELECT id,value,expiration from entries where KEY = $1 and (expiration IS NULL or expiration > $2)', [key, now], function (err, result) { |
|
||||||
if (err) { |
|
||||||
winston.error('error retrieving value from postgres', { error: err }); |
|
||||||
return callback(false); |
|
||||||
} |
|
||||||
callback(result.rows.length ? result.rows[0].value : false); |
|
||||||
if (result.rows.length && that.expireJS && !skipExpire) { |
|
||||||
client.query('UPDATE entries SET expiration = $1 WHERE ID = $2', [ |
|
||||||
that.expireJS + now, |
|
||||||
result.rows[0].id |
|
||||||
], function (err) { |
|
||||||
if (!err) { |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
} else { |
|
||||||
done(); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
}, |
|
||||||
|
|
||||||
// A connection wrapper
|
|
||||||
safeConnect: function (callback) { |
|
||||||
this.pool.connect((error, client, done) => { |
|
||||||
if (error) { |
|
||||||
winston.error('error connecting to postgres', {error}); |
|
||||||
callback(error); |
|
||||||
} else { |
|
||||||
callback(undefined, client, done); |
|
||||||
} |
|
||||||
}); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
module.exports = PostgresDocumentStore; |
|
@ -1,46 +0,0 @@ |
|||||||
const crypto = require('crypto'); |
|
||||||
const rethink = require('rethinkdbdash'); |
|
||||||
const winston = require('winston'); |
|
||||||
|
|
||||||
const md5 = (str) => { |
|
||||||
const md5sum = crypto.createHash('md5'); |
|
||||||
md5sum.update(str); |
|
||||||
return md5sum.digest('hex'); |
|
||||||
}; |
|
||||||
|
|
||||||
class RethinkDBStore { |
|
||||||
constructor(options) { |
|
||||||
this.client = rethink({ |
|
||||||
silent: true, |
|
||||||
host: options.host || '127.0.0.1', |
|
||||||
port: options.port || 28015, |
|
||||||
db: options.db || 'haste', |
|
||||||
user: options.user || 'admin', |
|
||||||
password: options.password || '' |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
set(key, data, callback) { |
|
||||||
this.client.table('uploads').insert({ id: md5(key), data: data }).run((error) => { |
|
||||||
if (error) { |
|
||||||
callback(false); |
|
||||||
winston.error('failed to insert to table', error); |
|
||||||
return; |
|
||||||
} |
|
||||||
callback(true); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
get(key, callback) { |
|
||||||
this.client.table('uploads').get(md5(key)).run((error, result) => { |
|
||||||
if (error || !result) { |
|
||||||
callback(false); |
|
||||||
if (error) winston.error('failed to insert to table', error); |
|
||||||
return; |
|
||||||
} |
|
||||||
callback(result.data); |
|
||||||
}); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
module.exports = RethinkDBStore; |
|
@ -1,32 +0,0 @@ |
|||||||
const fs = require('fs'); |
|
||||||
|
|
||||||
module.exports = class DictionaryGenerator { |
|
||||||
|
|
||||||
constructor(options, readyCallback) { |
|
||||||
// Check options format
|
|
||||||
if (!options) throw Error('No options passed to generator'); |
|
||||||
if (!options.path) throw Error('No dictionary path specified in options'); |
|
||||||
|
|
||||||
// Load dictionary
|
|
||||||
fs.readFile(options.path, 'utf8', (err, data) => { |
|
||||||
if (err) throw err; |
|
||||||
|
|
||||||
this.dictionary = data.split(/[\n\r]+/); |
|
||||||
|
|
||||||
if (readyCallback) readyCallback(); |
|
||||||
}); |
|
||||||
} |
|
||||||
|
|
||||||
// Generates a dictionary-based key, of keyLength words
|
|
||||||
createKey(keyLength) { |
|
||||||
let text = ''; |
|
||||||
|
|
||||||
for (let i = 0; i < keyLength; i++) { |
|
||||||
const index = Math.floor(Math.random() * this.dictionary.length); |
|
||||||
text += this.dictionary[index]; |
|
||||||
} |
|
||||||
|
|
||||||
return text; |
|
||||||
} |
|
||||||
|
|
||||||
}; |
|
@ -1,27 +1,32 @@ |
|||||||
// Draws inspiration from pwgen and http://tools.arantius.com/password
|
// Draws inspiration from pwgen and http://tools.arantius.com/password
|
||||||
|
var PhoneticKeyGenerator = function(options) { |
||||||
const randOf = (collection) => { |
// No options
|
||||||
return () => { |
|
||||||
return collection[Math.floor(Math.random() * collection.length)]; |
|
||||||
}; |
|
||||||
}; |
}; |
||||||
|
|
||||||
// Helper methods to get an random vowel or consonant
|
// Generate a phonetic key
|
||||||
const randVowel = randOf('aeiou'); |
PhoneticKeyGenerator.prototype.createKey = function(keyLength) { |
||||||
const randConsonant = randOf('bcdfghjklmnpqrstvwxyz'); |
var text = ''; |
||||||
|
for (var i = 0; i < keyLength; i++) { |
||||||
module.exports = class PhoneticKeyGenerator { |
text += (i % 2 == 0) ? this.randConsonant() : this.randVowel(); |
||||||
|
} |
||||||
// Generate a phonetic key of alternating consonant & vowel
|
return text; |
||||||
createKey(keyLength) { |
}; |
||||||
let text = ''; |
|
||||||
const start = Math.round(Math.random()); |
|
||||||
|
|
||||||
for (let i = 0; i < keyLength; i++) { |
PhoneticKeyGenerator.consonants = 'bcdfghjklmnpqrstvwxy'; |
||||||
text += (i % 2 == start) ? randConsonant() : randVowel(); |
PhoneticKeyGenerator.vowels = 'aeiou'; |
||||||
} |
|
||||||
|
|
||||||
return text; |
// Get an random vowel
|
||||||
} |
PhoneticKeyGenerator.prototype.randVowel = function() { |
||||||
|
return PhoneticKeyGenerator.vowels[ |
||||||
|
Math.floor(Math.random() * PhoneticKeyGenerator.vowels.length) |
||||||
|
]; |
||||||
|
}; |
||||||
|
|
||||||
|
// Get an random consonant
|
||||||
|
PhoneticKeyGenerator.prototype.randConsonant = function() { |
||||||
|
return PhoneticKeyGenerator.consonants[ |
||||||
|
Math.floor(Math.random() * PhoneticKeyGenerator.consonants.length) |
||||||
|
]; |
||||||
}; |
}; |
||||||
|
|
||||||
|
module.exports = PhoneticKeyGenerator; |
||||||
|
@ -1,20 +1,14 @@ |
|||||||
module.exports = class RandomKeyGenerator { |
var RandomKeyGenerator = function(options) { |
||||||
|
this.keyspace = options.keyspace || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
||||||
// Initialize a new generator with the given keySpace
|
}; |
||||||
constructor(options = {}) { |
|
||||||
this.keyspace = options.keyspace || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
|
||||||
} |
|
||||||
|
|
||||||
// Generate a key of the given length
|
|
||||||
createKey(keyLength) { |
|
||||||
var text = ''; |
|
||||||
|
|
||||||
for (var i = 0; i < keyLength; i++) { |
|
||||||
const index = Math.floor(Math.random() * this.keyspace.length); |
|
||||||
text += this.keyspace.charAt(index); |
|
||||||
} |
|
||||||
|
|
||||||
return text; |
// Generate a random key
|
||||||
|
RandomKeyGenerator.prototype.createKey = function(keyLength) { |
||||||
|
var text = ''; |
||||||
|
for (var i = 0; i < keyLength; i++) { |
||||||
|
text += this.keyspace.charAt(Math.floor(Math.random() * this.keyspace.length)); |
||||||
} |
} |
||||||
|
return text; |
||||||
}; |
}; |
||||||
|
|
||||||
|
module.exports = RandomKeyGenerator; |
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,47 +1,52 @@ |
|||||||
{ |
{ |
||||||
"name": "haste", |
|
||||||
"version": "0.1.0", |
"name": "haste", |
||||||
"private": true, |
"version": "0.0.1", |
||||||
"description": "Private Pastebin Server", |
|
||||||
"keywords": [ |
"private": true, |
||||||
"paste", |
|
||||||
"pastebin" |
"description": "Private Paste", |
||||||
], |
|
||||||
"author": { |
"keywords": [ "paste", "pastebin" ], |
||||||
"name": "John Crepezzi", |
|
||||||
"email": "john.crepezzi@gmail.com", |
"author": { |
||||||
"url": "http://seejohncode.com/" |
"name": "John Crepezzi", |
||||||
|
"email": "john.crepezzi@gmail.com", |
||||||
|
"url": "http://seejohncode.com/" |
||||||
|
}, |
||||||
|
|
||||||
|
"main": "haste", |
||||||
|
|
||||||
|
"dependencies": { |
||||||
|
"winston": "*", |
||||||
|
"hashlib": "*", |
||||||
|
"connect": "*", |
||||||
|
"uglify-js": "*" |
||||||
|
}, |
||||||
|
|
||||||
|
"devDependencies": { |
||||||
|
"jasmine-node": "*" |
||||||
}, |
}, |
||||||
"main": "haste", |
|
||||||
"dependencies": { |
"bundledDependencies": [], |
||||||
"busboy": "0.2.4", |
|
||||||
"connect": "^3.7.0", |
"engines": { |
||||||
"connect-ratelimit": "0.0.7", |
"node": "*" |
||||||
"connect-route": "0.1.5", |
}, |
||||||
"pg": "^8.0.0", |
|
||||||
"redis": "0.8.1", |
"bin": { |
||||||
"redis-url": "0.1.0", |
"haste-server": "./server.js" |
||||||
"st": "^2.0.0", |
}, |
||||||
"uglify-js": "3.1.6", |
|
||||||
"winston": "^2.0.0" |
"files": [ "server.js", "lib", "static" ], |
||||||
}, |
|
||||||
"devDependencies": { |
"directories": { |
||||||
"mocha": "^8.1.3" |
"lib": "./lib" |
||||||
}, |
}, |
||||||
"bundledDependencies": [], |
|
||||||
"bin": { |
"scripts": { |
||||||
"haste-server": "./server.js" |
"start": "node server.js", |
||||||
}, |
"test": "jasmine-node spec" |
||||||
"files": [ |
} |
||||||
"server.js", |
|
||||||
"lib", |
|
||||||
"static" |
|
||||||
], |
|
||||||
"directories": { |
|
||||||
"lib": "./lib" |
|
||||||
}, |
|
||||||
"scripts": { |
|
||||||
"start": "node server.js", |
|
||||||
"test": "mocha --recursive" |
|
||||||
} |
|
||||||
} |
} |
||||||
|
@ -0,0 +1,19 @@ |
|||||||
|
var DocumentHandler = require('../lib/document_handler'); |
||||||
|
|
||||||
|
describe('document_handler', function() { |
||||||
|
|
||||||
|
describe('randomKey', function() { |
||||||
|
|
||||||
|
it('should choose a key of the proper length', function() { |
||||||
|
var dh = new DocumentHandler({ keyLength: 6 }); |
||||||
|
expect(dh.randomKey().length).toBe(6); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should choose a default key length', function() { |
||||||
|
var dh = new DocumentHandler(); |
||||||
|
expect(dh.keyLength).toBe(DocumentHandler.defaultKeyLength); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
@ -0,0 +1,89 @@ |
|||||||
|
var RedisDocumentStore = require('../lib/redis_document_store'); |
||||||
|
|
||||||
|
var winston = require('winston'); |
||||||
|
winston.remove(winston.transports.Console); |
||||||
|
|
||||||
|
describe('redis_document_store', function() { |
||||||
|
|
||||||
|
/* reconnect to redis on each test */ |
||||||
|
afterEach(function() { |
||||||
|
if (RedisDocumentStore.client) { |
||||||
|
RedisDocumentStore.client.quit(); |
||||||
|
RedisDocumentStore.client = false; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
describe('set', function() { |
||||||
|
|
||||||
|
it('should be able to set a key and have an expiration set', function() { |
||||||
|
var store = new RedisDocumentStore({ expire: 10 }); |
||||||
|
runs(function() { |
||||||
|
var _this = this; |
||||||
|
store.set('hello1', 'world', function(worked) { |
||||||
|
_this.result = worked;
|
||||||
|
}); |
||||||
|
}); |
||||||
|
waitsFor(function() { |
||||||
|
return typeof(this.result) === 'boolean'; |
||||||
|
}); |
||||||
|
runs(function() { |
||||||
|
var _this = this; |
||||||
|
RedisDocumentStore.client.ttl('hello1', function(err, res) { |
||||||
|
expect(res).toBeGreaterThan(1); |
||||||
|
_this.done = true; |
||||||
|
}); |
||||||
|
}); |
||||||
|
waitsFor(function() { |
||||||
|
return this.done; |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should not set an expiration when told not to', function() { |
||||||
|
var store = new RedisDocumentStore({ expire: 10 }); |
||||||
|
runs(function() { |
||||||
|
var _this = this; |
||||||
|
store.set('hello2', 'world', function(worked) { |
||||||
|
_this.result = worked;
|
||||||
|
}, true); |
||||||
|
}); |
||||||
|
waitsFor(function() { |
||||||
|
return typeof(this.result) === 'boolean'; |
||||||
|
}); |
||||||
|
runs(function() { |
||||||
|
var _this = this; |
||||||
|
RedisDocumentStore.client.ttl('hello2', function(err, res) { |
||||||
|
expect(res).toBe(-1); |
||||||
|
_this.done = true; |
||||||
|
}); |
||||||
|
}); |
||||||
|
waitsFor(function() { |
||||||
|
return this.done; |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should not set an expiration when expiration is off', function() { |
||||||
|
var store = new RedisDocumentStore({ expire: false }); |
||||||
|
runs(function() { |
||||||
|
var _this = this; |
||||||
|
store.set('hello3', 'world', function(worked) { |
||||||
|
_this.result = worked;
|
||||||
|
}); |
||||||
|
}); |
||||||
|
waitsFor(function() { |
||||||
|
return typeof(this.result) === 'boolean'; |
||||||
|
}); |
||||||
|
runs(function() { |
||||||
|
var _this = this; |
||||||
|
RedisDocumentStore.client.ttl('hello3', function(err, res) { |
||||||
|
expect(res).toBe(-1); |
||||||
|
_this.done = true; |
||||||
|
}); |
||||||
|
}); |
||||||
|
waitsFor(function() { |
||||||
|
return this.done; |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
}); |
||||||
|
|
||||||
|
}); |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.7 KiB |
File diff suppressed because one or more lines are too long
@ -1,26 +0,0 @@ |
|||||||
/* global describe, it */ |
|
||||||
|
|
||||||
var assert = require('assert'); |
|
||||||
|
|
||||||
var DocumentHandler = require('../lib/document_handler'); |
|
||||||
var Generator = require('../lib/key_generators/random'); |
|
||||||
|
|
||||||
describe('document_handler', function() { |
|
||||||
|
|
||||||
describe('randomKey', function() { |
|
||||||
|
|
||||||
it('should choose a key of the proper length', function() { |
|
||||||
var gen = new Generator(); |
|
||||||
var dh = new DocumentHandler({ keyLength: 6, keyGenerator: gen }); |
|
||||||
assert.equal(6, dh.acceptableKey().length); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should choose a default key length', function() { |
|
||||||
var gen = new Generator(); |
|
||||||
var dh = new DocumentHandler({ keyGenerator: gen }); |
|
||||||
assert.equal(dh.keyLength, DocumentHandler.defaultKeyLength); |
|
||||||
}); |
|
||||||
|
|
||||||
}); |
|
||||||
|
|
||||||
}); |
|
@ -1,34 +0,0 @@ |
|||||||
/* global describe, it */ |
|
||||||
|
|
||||||
const assert = require('assert'); |
|
||||||
|
|
||||||
const fs = require('fs'); |
|
||||||
|
|
||||||
const Generator = require('../../lib/key_generators/dictionary'); |
|
||||||
|
|
||||||
describe('DictionaryGenerator', function() { |
|
||||||
describe('options', function() { |
|
||||||
it('should throw an error if given no options', () => { |
|
||||||
assert.throws(() => { |
|
||||||
new Generator(); |
|
||||||
}, Error); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should throw an error if given no path', () => { |
|
||||||
assert.throws(() => { |
|
||||||
new Generator({}); |
|
||||||
}, Error); |
|
||||||
}); |
|
||||||
}); |
|
||||||
describe('generation', function() { |
|
||||||
it('should return a key of the proper number of words from the given dictionary', () => { |
|
||||||
const path = '/tmp/haste-server-test-dictionary'; |
|
||||||
const words = ['cat']; |
|
||||||
fs.writeFileSync(path, words.join('\n')); |
|
||||||
|
|
||||||
const gen = new Generator({path}, () => { |
|
||||||
assert.equal('catcatcat', gen.createKey(3)); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,35 +0,0 @@ |
|||||||
/* global describe, it */ |
|
||||||
|
|
||||||
const assert = require('assert'); |
|
||||||
|
|
||||||
const Generator = require('../../lib/key_generators/phonetic'); |
|
||||||
|
|
||||||
const vowels = 'aeiou'; |
|
||||||
const consonants = 'bcdfghjklmnpqrstvwxyz'; |
|
||||||
|
|
||||||
describe('PhoneticKeyGenerator', () => { |
|
||||||
describe('generation', () => { |
|
||||||
it('should return a key of the proper length', () => { |
|
||||||
const gen = new Generator(); |
|
||||||
assert.equal(6, gen.createKey(6).length); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should alternate consonants and vowels', () => { |
|
||||||
const gen = new Generator(); |
|
||||||
|
|
||||||
const key = gen.createKey(3); |
|
||||||
|
|
||||||
// if it starts with a consonant, we expect cvc
|
|
||||||
// if it starts with a vowel, we expect vcv
|
|
||||||
if(consonants.includes(key[0])) { |
|
||||||
assert.ok(consonants.includes(key[0])); |
|
||||||
assert.ok(consonants.includes(key[2])); |
|
||||||
assert.ok(vowels.includes(key[1])); |
|
||||||
} else { |
|
||||||
assert.ok(vowels.includes(key[0])); |
|
||||||
assert.ok(vowels.includes(key[2])); |
|
||||||
assert.ok(consonants.includes(key[1])); |
|
||||||
} |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,24 +0,0 @@ |
|||||||
/* global describe, it */ |
|
||||||
|
|
||||||
const assert = require('assert'); |
|
||||||
|
|
||||||
const Generator = require('../../lib/key_generators/random'); |
|
||||||
|
|
||||||
describe('RandomKeyGenerator', () => { |
|
||||||
describe('generation', () => { |
|
||||||
it('should return a key of the proper length', () => { |
|
||||||
const gen = new Generator(); |
|
||||||
assert.equal(gen.createKey(6).length, 6); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should use a key from the given keyset if given', () => { |
|
||||||
const gen = new Generator({keyspace: 'A'}); |
|
||||||
assert.equal(gen.createKey(6), 'AAAAAA'); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should not use a key from the given keyset if not given', () => { |
|
||||||
const gen = new Generator({keyspace: 'A'}); |
|
||||||
assert.ok(!gen.createKey(6).includes('B')); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
@ -1,54 +0,0 @@ |
|||||||
/* global it, describe, afterEach */ |
|
||||||
|
|
||||||
var assert = require('assert'); |
|
||||||
|
|
||||||
var winston = require('winston'); |
|
||||||
winston.remove(winston.transports.Console); |
|
||||||
|
|
||||||
var RedisDocumentStore = require('../lib/document_stores/redis'); |
|
||||||
|
|
||||||
describe('redis_document_store', function() { |
|
||||||
|
|
||||||
/* reconnect to redis on each test */ |
|
||||||
afterEach(function() { |
|
||||||
if (RedisDocumentStore.client) { |
|
||||||
RedisDocumentStore.client.quit(); |
|
||||||
RedisDocumentStore.client = false; |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
describe('set', function() { |
|
||||||
|
|
||||||
it('should be able to set a key and have an expiration set', function(done) { |
|
||||||
var store = new RedisDocumentStore({ expire: 10 }); |
|
||||||
store.set('hello1', 'world', function() { |
|
||||||
RedisDocumentStore.client.ttl('hello1', function(err, res) { |
|
||||||
assert.ok(res > 1); |
|
||||||
done(); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should not set an expiration when told not to', function(done) { |
|
||||||
var store = new RedisDocumentStore({ expire: 10 }); |
|
||||||
store.set('hello2', 'world', function() { |
|
||||||
RedisDocumentStore.client.ttl('hello2', function(err, res) { |
|
||||||
assert.equal(-1, res); |
|
||||||
done(); |
|
||||||
}); |
|
||||||
}, true); |
|
||||||
}); |
|
||||||
|
|
||||||
it('should not set an expiration when expiration is off', function(done) { |
|
||||||
var store = new RedisDocumentStore({ expire: false }); |
|
||||||
store.set('hello3', 'world', function() { |
|
||||||
RedisDocumentStore.client.ttl('hello3', function(err, res) { |
|
||||||
assert.equal(-1, res); |
|
||||||
done(); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
||||||
|
|
||||||
}); |
|
||||||
|
|
||||||
}); |
|
Loading…
Reference in new issue