Compare commits
No commits in common. 'gh-pages' and 'master' have entirely different histories.
50 changed files with 4335 additions and 385 deletions
@ -0,0 +1,8 @@ |
||||
Dockerfile |
||||
.git |
||||
npm-debug.log |
||||
node_modules |
||||
*.swp |
||||
*.swo |
||||
data |
||||
*.DS_Store |
@ -0,0 +1,2 @@ |
||||
**/*.min.js |
||||
config.js |
@ -0,0 +1,25 @@ |
||||
{ |
||||
"env": { |
||||
"es6": true, |
||||
"node": true |
||||
}, |
||||
"extends": "eslint:recommended", |
||||
"rules": { |
||||
"indent": [ |
||||
"error", |
||||
2 |
||||
], |
||||
"linebreak-style": [ |
||||
"error", |
||||
"unix" |
||||
], |
||||
"quotes": [ |
||||
"error", |
||||
"single" |
||||
], |
||||
"semi": [ |
||||
"error", |
||||
"always" |
||||
] |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
* @toptal/site-acquisition-eng |
@ -0,0 +1,30 @@ |
||||
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 }} |
@ -0,0 +1,7 @@ |
||||
npm-debug.log |
||||
node_modules |
||||
*.swp |
||||
*.swo |
||||
data |
||||
*.DS_Store |
||||
docker-compose.override.yml |
@ -0,0 +1,68 @@ |
||||
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"] |
@ -0,0 +1,389 @@ |
||||
# Haste |
||||
|
||||
haste is an open-source pastebin software written in node.js, which is easily |
||||
installable in any network. It can be backed by either redis or filesystem, |
||||
and has a very easy adapter interface for other stores. A publicly available |
||||
version can be found at [hastebin.com](http://hastebin.com) |
||||
|
||||
Major design objectives: |
||||
|
||||
* Be really pretty |
||||
* Be really simple |
||||
* Be easy to set up and use |
||||
|
||||
Haste works really well with a little utility called |
||||
[haste-client](https://github.com/seejohnrun/haste-client), allowing you |
||||
to do things like: |
||||
|
||||
`cat something | haste` |
||||
|
||||
which will output a URL to share containing the contents of `cat something`'s |
||||
STDOUT. Check the README there for more details and usages. |
||||
|
||||
## Tested Browsers |
||||
|
||||
* Firefox 8 |
||||
* Chrome 17 |
||||
* Safari 5.3 |
||||
|
||||
## Installation |
||||
|
||||
1. Download the package, and expand it |
||||
2. Explore the settings inside of config.js, but the defaults should be good |
||||
3. `npm install` |
||||
4. `npm start` (you may specify an optional `<config-path>` as well) |
||||
|
||||
## Settings |
||||
|
||||
* `host` - the host the server runs on (default localhost) |
||||
* `port` - the port the server runs on (default 7777) |
||||
* `keyLength` - the length of the keys to user (default 10) |
||||
* `maxLength` - maximum length of a paste (default 400000) |
||||
* `staticMaxAge` - max age for static assets (86400) |
||||
* `recompressStaticAssets` - whether or not to compile static js assets (true) |
||||
* `documents` - static documents to serve (ex: http://hastebin.com/about.com) |
||||
in addition to static assets. These will never expire. |
||||
* `storage` - storage options (see below) |
||||
* `logging` - logging preferences |
||||
* `keyGenerator` - key generator options (see below) |
||||
* `rateLimits` - settings for rate limiting (see below) |
||||
|
||||
## Rate Limiting |
||||
|
||||
When present, the `rateLimits` option enables built-in rate limiting courtesy |
||||
of `connect-ratelimit`. Any of the options supported by that library can be |
||||
used and set in `config.js`. |
||||
|
||||
See the README for [connect-ratelimit](https://github.com/dharmafly/connect-ratelimit) |
||||
for more information! |
||||
|
||||
## Key Generation |
||||
|
||||
### Phonetic |
||||
|
||||
Attempts to generate phonetic keys, similar to `pwgen` |
||||
|
||||
``` json |
||||
{ |
||||
"type": "phonetic" |
||||
} |
||||
``` |
||||
|
||||
### Random |
||||
|
||||
Generates a random key |
||||
|
||||
``` json |
||||
{ |
||||
"type": "random", |
||||
"keyspace": "abcdef" |
||||
} |
||||
``` |
||||
|
||||
The _optional_ keySpace argument is a string of acceptable characters |
||||
for the key. |
||||
|
||||
## Storage |
||||
|
||||
### File |
||||
|
||||
To use file storage (the default) change the storage section in `config.js` to |
||||
something like: |
||||
|
||||
``` json |
||||
{ |
||||
"path": "./data", |
||||
"type": "file" |
||||
} |
||||
``` |
||||
|
||||
where `path` represents where you want the files stored. |
||||
|
||||
File storage currently does not support paste expiration, you can follow [#191](https://github.com/seejohnrun/haste-server/issues/191) for status updates. |
||||
|
||||
### Redis |
||||
|
||||
To use redis storage you must install the `redis` package in npm, and have |
||||
`redis-server` running on the machine. |
||||
|
||||
`npm install redis` |
||||
|
||||
Once you've done that, your config section should look like: |
||||
|
||||
``` json |
||||
{ |
||||
"type": "redis", |
||||
"host": "localhost", |
||||
"port": 6379, |
||||
"db": 2 |
||||
} |
||||
``` |
||||
|
||||
You can also set an `expire` option to the number of seconds to expire keys in. |
||||
This is off by default, but will constantly kick back expirations on each view |
||||
or post. |
||||
|
||||
All of which are optional except `type` with very logical default values. |
||||
|
||||
If your Redis server is configured for password authentification, use the `password` field. |
||||
|
||||
### Postgres |
||||
|
||||
To use postgres storage you must install the `pg` package in npm |
||||
|
||||
`npm install pg` |
||||
|
||||
Once you've done that, your config section should look like: |
||||
|
||||
``` json |
||||
{ |
||||
"type": "postgres", |
||||
"connectionUrl": "postgres://user:password@host:5432/database" |
||||
} |
||||
``` |
||||
|
||||
You can also just set the environment variable for `DATABASE_URL` to your database connection url. |
||||
|
||||
You will have to manually add a table to your postgres database: |
||||
|
||||
`create table entries (id serial primary key, key varchar(255) not null, value text not null, expiration int, unique(key));` |
||||
|
||||
You can also set an `expire` option to the number of seconds to expire keys in. |
||||
This is off by default, but will constantly kick back expirations on each view |
||||
or post. |
||||
|
||||
All of which are optional except `type` with very logical default values. |
||||
|
||||
### MongoDB |
||||
|
||||
To use mongodb storage you must install the 'mongodb' package in npm |
||||
|
||||
`npm install mongodb` |
||||
|
||||
Once you've done that, your config section should look like: |
||||
|
||||
``` json |
||||
{ |
||||
"type": "mongo", |
||||
"connectionUrl": "mongodb://localhost:27017/database" |
||||
} |
||||
``` |
||||
|
||||
You can also just set the environment variable for `DATABASE_URL` to your database connection url. |
||||
|
||||
Unlike with postgres you do NOT have to create the table in your mongo database prior to running. |
||||
|
||||
You can also set an `expire` option to the number of seconds to expire keys in. |
||||
This is off by default, but will constantly kick back expirations on each view or post. |
||||
|
||||
### Memcached |
||||
|
||||
To use memcache storage you must install the `memcached` package via npm |
||||
|
||||
`npm install memcached` |
||||
|
||||
Once you've done that, your config section should look like: |
||||
|
||||
``` json |
||||
{ |
||||
"type": "memcached", |
||||
"host": "127.0.0.1", |
||||
"port": 11211 |
||||
} |
||||
``` |
||||
|
||||
You can also set an `expire` option to the number of seconds to expire keys in. |
||||
This behaves just like the redis expirations, but does not push expirations |
||||
forward on GETs. |
||||
|
||||
All of which are optional except `type` with very logical default values. |
||||
|
||||
### RethinkDB |
||||
|
||||
To use the RethinkDB storage system, you must install the `rethinkdbdash` package via npm |
||||
|
||||
`npm install rethinkdbdash` |
||||
|
||||
Once you've done that, your config section should look like this: |
||||
|
||||
``` json |
||||
{ |
||||
"type": "rethinkdb", |
||||
"host": "127.0.0.1", |
||||
"port": 28015, |
||||
"db": "haste" |
||||
} |
||||
``` |
||||
|
||||
In order for this to work, the database must be pre-created before the script is ran. |
||||
Also, you must create an `uploads` table, which will store all the data for uploads. |
||||
|
||||
You can optionally add the `user` and `password` properties to use a user system. |
||||
|
||||
### Google Datastore |
||||
|
||||
To use the Google Datastore storage system, you must install the `@google-cloud/datastore` package via npm |
||||
|
||||
`npm install @google-cloud/datastore` |
||||
|
||||
Once you've done that, your config section should look like this: |
||||
|
||||
``` json |
||||
{ |
||||
"type": "google-datastore" |
||||
} |
||||
``` |
||||
|
||||
Authentication is handled automatically by [Google Cloud service account credentials](https://cloud.google.com/docs/authentication/getting-started), by providing authentication details to the GOOGLE_APPLICATION_CREDENTIALS environmental variable. |
||||
|
||||
### Amazon S3 |
||||
|
||||
To use [Amazon S3](https://aws.amazon.com/s3/) as a storage system, you must |
||||
install the `aws-sdk` package via npm: |
||||
|
||||
`npm install aws-sdk` |
||||
|
||||
Once you've done that, your config section should look like this: |
||||
|
||||
```json |
||||
{ |
||||
"type": "amazon-s3", |
||||
"bucket": "your-bucket-name", |
||||
"region": "us-east-1" |
||||
} |
||||
``` |
||||
|
||||
Authentication is handled automatically by the client. Check |
||||
[Amazon's documentation](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) |
||||
for more information. You will need to grant your role these permissions to |
||||
your bucket: |
||||
|
||||
```json |
||||
{ |
||||
"Version": "2012-10-17", |
||||
"Statement": [ |
||||
{ |
||||
"Action": [ |
||||
"s3:GetObject", |
||||
"s3:PutObject" |
||||
], |
||||
"Effect": "Allow", |
||||
"Resource": "arn:aws:s3:::your-bucket-name-goes-here/*" |
||||
} |
||||
] |
||||
} |
||||
``` |
||||
|
||||
## Docker |
||||
|
||||
### Build image |
||||
|
||||
```bash |
||||
docker build --tag haste-server . |
||||
``` |
||||
|
||||
### Run container |
||||
|
||||
For this example we will run haste-server, and connect it to a redis server |
||||
|
||||
```bash |
||||
docker run --name haste-server-container --env STORAGE_TYPE=redis --env STORAGE_HOST=redis-server --env STORAGE_PORT=6379 haste-server |
||||
``` |
||||
|
||||
### Use docker-compose example |
||||
|
||||
There is an example `docker-compose.yml` which runs haste-server together with memcached |
||||
|
||||
```bash |
||||
docker-compose up |
||||
``` |
||||
|
||||
### Configuration |
||||
|
||||
The docker image is configured using environmental variables as you can see in the example above. |
||||
|
||||
Here is a list of all the environment variables |
||||
|
||||
### Storage |
||||
|
||||
| Name | Default value | Description | |
||||
| :--------------------: | :-----------: | :-----------------------------------------------------------------------------------------------------------: | |
||||
| STORAGE_TYPE | memcached | Type of storage . Accepted values: "memcached","redis","postgres","rethinkdb", "amazon-s3", and "file" | |
||||
| STORAGE_HOST | 127.0.0.1 | Storage host. Applicable for types: memcached, redis, postgres, and rethinkdb | |
||||
| STORAGE_PORT | 11211 | Port on the storage host. Applicable for types: memcached, redis, postgres, and rethinkdb | |
||||
| STORAGE_EXPIRE_SECONDS | 2592000 | Number of seconds to expire keys in. Applicable for types. Redis, postgres, memcached. `expire` option to the | |
||||
| STORAGE_DB | 2 | The name of the database. Applicable for redis, postgres, and rethinkdb | |
||||
| STORAGE_PASSWORD | | Password for database. Applicable for redis, postges, rethinkdb . | |
||||
| STORAGE_USERNAME | | Database username. Applicable for postgres, and rethinkdb | |
||||
| STORAGE_AWS_BUCKET | | Applicable for amazon-s3. This is the name of the S3 bucket | |
||||
| STORAGE_AWS_REGION | | Applicable for amazon-s3. The region in which the bucket is located | |
||||
| STORAGE_FILEPATH | | Path to file to save data to. Applicable for type file | |
||||
|
||||
### Logging |
||||
|
||||
| Name | Default value | Description | |
||||
| :---------------: | :-----------: | :---------: | |
||||
| LOGGING_LEVEL | verbose | | |
||||
| LOGGING_TYPE= | Console | |
||||
| LOGGING_COLORIZE= | true | |
||||
|
||||
### Basics |
||||
|
||||
| Name | Default value | Description | |
||||
| :----------------------: | :--------------: | :---------------------------------------------------------------------------------------: | |
||||
| HOST | 0.0.0.0 | The hostname which the server answers on | |
||||
| PORT | 7777 | The port on which the server is running | |
||||
| KEY_LENGTH | 10 | the length of the keys to user | |
||||
| MAX_LENGTH | 400000 | maximum length of a paste | |
||||
| STATIC_MAX_AGE | 86400 | max age for static assets | |
||||
| RECOMPRESS_STATIC_ASSETS | true | whether or not to compile static js assets | |
||||
| KEYGENERATOR_TYPE | phonetic | Type of key generator. Acceptable values: "phonetic", or "random" | |
||||
| KEYGENERATOR_KEYSPACE | | keySpace argument is a string of acceptable characters | |
||||
| DOCUMENTS | about=./about.md | Comma separated list of static documents to serve. ex: \n about=./about.md,home=./home.md | |
||||
|
||||
### Rate limits |
||||
|
||||
| Name | Default value | Description | |
||||
| :----------------------------------: | :-----------------------------------: | :--------------------------------------------------------------------------------------: | |
||||
| RATELIMITS_NORMAL_TOTAL_REQUESTS | 500 | By default anyone uncategorized will be subject to 500 requests in the defined timespan. | |
||||
| RATELIMITS_NORMAL_EVERY_MILLISECONDS | 60000 | The timespan to allow the total requests for uncategorized users | |
||||
| RATELIMITS_WHITELIST_TOTAL_REQUESTS | | By default client names in the whitelist will not have their requests limited. | |
||||
| RATELIMITS_WHITELIST_EVERY_SECONDS | | By default client names in the whitelist will not have their requests limited. | |
||||
| RATELIMITS_WHITELIST | example1.whitelist,example2.whitelist | Comma separated list of the clients which are in the whitelist pool | |
||||
| RATELIMITS_BLACKLIST_TOTAL_REQUESTS | | By default client names in the blacklist will be subject to 0 requests per hours. | |
||||
| RATELIMITS_BLACKLIST_EVERY_SECONDS | | By default client names in the blacklist will be subject to 0 requests per hours | |
||||
| RATELIMITS_BLACKLIST | example1.blacklist,example2.blacklist | Comma separated list of the clients which are in the blacklistpool. | |
||||
|
||||
## Author |
||||
|
||||
John Crepezzi <john.crepezzi@gmail.com> |
||||
|
||||
## License |
||||
|
||||
(The MIT License) |
||||
|
||||
Copyright © 2011-2012 John Crepezzi |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the ‘Software’), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies |
||||
of the Software, and to permit persons to whom the Software is furnished to do |
||||
so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED ‘AS IS’, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE |
||||
|
||||
### Other components: |
||||
|
||||
* jQuery: MIT/GPL license |
||||
* highlight.js: Copyright © 2006, Ivan Sagalaev |
||||
* highlightjs-coffeescript: WTFPL - Copyright © 2011, Dmytrii Nagirniak |
@ -0,0 +1,61 @@ |
||||
# Haste |
||||
|
||||
Sharing code is a good thing, and it should be _really_ easy to do it. |
||||
A lot of times, I want to show you something I'm seeing - and that's where we |
||||
use pastebins. |
||||
|
||||
Haste is the prettiest, easiest to use pastebin ever made. |
||||
|
||||
## Basic Usage |
||||
|
||||
Type what you want me to see, click "Save", and then copy the URL. Send that |
||||
URL to someone and they'll see what you see. |
||||
|
||||
To make a new entry, click "New" (or type 'control + n') |
||||
|
||||
## From the Console |
||||
|
||||
Most of the time I want to show you some text, it's coming from my current |
||||
console session. We should make it really easy to take code from the console |
||||
and send it to people. |
||||
|
||||
`cat something | haste` # https://hastebin.com/1238193 |
||||
|
||||
You can even take this a step further, and cut out the last step of copying the |
||||
URL with: |
||||
|
||||
* osx: `cat something | haste | pbcopy` |
||||
* linux: `cat something | haste | xsel -b` |
||||
* windows: check out [WinHaste](https://github.com/ajryan/WinHaste) |
||||
|
||||
After running that, the STDOUT output of `cat something` will show up at a URL |
||||
which has been conveniently copied to your clipboard. |
||||
|
||||
That's all there is to that, and you can install it with `gem install haste` |
||||
right now. |
||||
* osx: you will need to have an up to date version of Xcode |
||||
* linux: you will need to have rubygems and ruby-devel installed |
||||
|
||||
## Duration |
||||
|
||||
Pastes will stay for 30 days from their last view. They may be removed earlier |
||||
and without notice. |
||||
|
||||
## Privacy |
||||
|
||||
While the contents of hastebin.com are not directly crawled by any search robot |
||||
that obeys "robots.txt", there should be no great expectation of privacy. Post |
||||
things at your own risk. Not responsible for any loss of data or removed |
||||
pastes. |
||||
|
||||
## Open Source |
||||
|
||||
Haste can easily be installed behind your network, and it's all open source! |
||||
|
||||
* [haste-client](https://github.com/seejohnrun/haste-client) |
||||
* [haste-server](https://github.com/seejohnrun/haste-server) |
||||
|
||||
## Author |
||||
|
||||
Code by John Crepezzi <john.crepezzi@gmail.com> |
||||
Key Design by Brian Dawson <bridawson@gmail.com> |
@ -0,0 +1,43 @@ |
||||
{ |
||||
|
||||
"host": "0.0.0.0", |
||||
"port": 7777, |
||||
|
||||
"keyLength": 10, |
||||
|
||||
"maxLength": 400000, |
||||
|
||||
"staticMaxAge": 86400, |
||||
|
||||
"recompressStaticAssets": true, |
||||
|
||||
"logging": [ |
||||
{ |
||||
"level": "verbose", |
||||
"type": "Console", |
||||
"colorize": true |
||||
} |
||||
], |
||||
|
||||
"keyGenerator": { |
||||
"type": "phonetic" |
||||
}, |
||||
|
||||
"rateLimits": { |
||||
"categories": { |
||||
"normal": { |
||||
"totalRequests": 500, |
||||
"every": 60000 |
||||
} |
||||
} |
||||
}, |
||||
|
||||
"storage": { |
||||
"type": "file" |
||||
}, |
||||
|
||||
"documents": { |
||||
"about": "./about.md" |
||||
} |
||||
|
||||
} |
@ -0,0 +1,12 @@ |
||||
version: '3.0' |
||||
services: |
||||
haste-server: |
||||
build: . |
||||
environment: |
||||
- STORAGE_TYPE=memcached |
||||
- STORAGE_HOST=memcached |
||||
- STORAGE_PORT=11211 |
||||
ports: |
||||
- 7777:7777 |
||||
memcached: |
||||
image: memcached:latest |
@ -0,0 +1,108 @@ |
||||
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)); |
@ -0,0 +1,9 @@ |
||||
#!/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,43 +0,0 @@ |
||||
<!doctype html> |
||||
<html> |
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta http-equiv="X-UA-Compatible" content="chrome=1"> |
||||
<title>Haste-server by seejohnrun</title> |
||||
|
||||
<link rel="stylesheet" href="stylesheets/styles.css"> |
||||
<link rel="stylesheet" href="stylesheets/pygment_trac.css"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no"> |
||||
<!--[if lt IE 9]> |
||||
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script> |
||||
<![endif]--> |
||||
</head> |
||||
<body> |
||||
<div class="wrapper"> |
||||
<header> |
||||
<h1>Haste-server</h1> |
||||
<p>open source pastebin written in node.js</p> |
||||
<p class="view"><a href="https://github.com/seejohnrun/haste-server">View the Project on GitHub <small>seejohnrun/haste-server</small></a></p> |
||||
<ul> |
||||
<li><a href="https://github.com/seejohnrun/haste-server/zipball/master">Download <strong>ZIP File</strong></a></li> |
||||
<li><a href="https://github.com/seejohnrun/haste-server/tarball/master">Download <strong>TAR Ball</strong></a></li> |
||||
<li><a href="https://github.com/seejohnrun/haste-server">Fork On <strong>GitHub</strong></a></li> |
||||
</ul> |
||||
</header> |
||||
<section> |
||||
<h1>Pastebin</h1> |
||||
|
||||
<p>haste-server is an open source pastebin. You can see an example at <a href="http://hastebin.com">hastebin.com</a>, and it's easy to install locally.</p> |
||||
|
||||
<h2>GitHub</h2> |
||||
|
||||
<p>Check out the project on <a href="https://github.com/seejohnrun/haste-server">github</a></p> |
||||
</section> |
||||
<footer> |
||||
<p>This project is maintained by <a href="https://github.com/seejohnrun">seejohnrun</a></p> |
||||
<p><small>Hosted on GitHub Pages — Theme by <a href="https://github.com/orderedlist">orderedlist</a></small></p> |
||||
</footer> |
||||
</div> |
||||
<script src="javascripts/scale.fix.js"></script> |
||||
</body> |
||||
</html> |
@ -1,17 +0,0 @@ |
||||
var metas = document.getElementsByTagName('meta'); |
||||
var i; |
||||
if (navigator.userAgent.match(/iPhone/i)) { |
||||
for (i=0; i<metas.length; i++) { |
||||
if (metas[i].name == "viewport") { |
||||
metas[i].content = "width=device-width, minimum-scale=1.0, maximum-scale=1.0"; |
||||
} |
||||
} |
||||
document.addEventListener("gesturestart", gestureStart, false); |
||||
} |
||||
function gestureStart() { |
||||
for (i=0; i<metas.length; i++) { |
||||
if (metas[i].name == "viewport") { |
||||
metas[i].content = "width=device-width, minimum-scale=0.25, maximum-scale=1.6"; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,155 @@ |
||||
var winston = require('winston'); |
||||
var Busboy = require('busboy'); |
||||
|
||||
// For handling serving stored documents
|
||||
|
||||
var DocumentHandler = function(options) { |
||||
if (!options) { |
||||
options = {}; |
||||
} |
||||
this.keyLength = options.keyLength || DocumentHandler.defaultKeyLength; |
||||
this.maxLength = options.maxLength; // none by default
|
||||
this.store = options.store; |
||||
this.keyGenerator = options.keyGenerator; |
||||
}; |
||||
|
||||
DocumentHandler.defaultKeyLength = 10; |
||||
|
||||
// Handle retrieving a document
|
||||
DocumentHandler.prototype.handleGet = function(request, response, config) { |
||||
const key = request.params.id.split('.')[0]; |
||||
const skipExpire = !!config.documents[key]; |
||||
|
||||
this.store.get(key, function(ret) { |
||||
if (ret) { |
||||
winston.verbose('retrieved document', { key: key }); |
||||
response.writeHead(200, { 'content-type': 'application/json' }); |
||||
if (request.method === 'HEAD') { |
||||
response.end(); |
||||
} else { |
||||
response.end(JSON.stringify({ data: ret, key: key })); |
||||
} |
||||
} |
||||
else { |
||||
winston.warn('document not found', { key: key }); |
||||
response.writeHead(404, { 'content-type': 'application/json' }); |
||||
if (request.method === 'HEAD') { |
||||
response.end(); |
||||
} else { |
||||
response.end(JSON.stringify({ message: 'Document not found.' })); |
||||
} |
||||
} |
||||
}, skipExpire); |
||||
}; |
||||
|
||||
// Handle retrieving the raw version of a document
|
||||
DocumentHandler.prototype.handleRawGet = function(request, response, config) { |
||||
const key = request.params.id.split('.')[0]; |
||||
const skipExpire = !!config.documents[key]; |
||||
|
||||
this.store.get(key, function(ret) { |
||||
if (ret) { |
||||
winston.verbose('retrieved raw document', { key: key }); |
||||
response.writeHead(200, { 'content-type': 'text/plain; charset=UTF-8' }); |
||||
if (request.method === 'HEAD') { |
||||
response.end(); |
||||
} else { |
||||
response.end(ret); |
||||
} |
||||
} |
||||
else { |
||||
winston.warn('raw document not found', { key: key }); |
||||
response.writeHead(404, { 'content-type': 'application/json' }); |
||||
if (request.method === 'HEAD') { |
||||
response.end(); |
||||
} else { |
||||
response.end(JSON.stringify({ message: 'Document not found.' })); |
||||
} |
||||
} |
||||
}, skipExpire); |
||||
}; |
||||
|
||||
// Handle adding a new Document
|
||||
DocumentHandler.prototype.handlePost = function (request, response) { |
||||
var _this = this; |
||||
var buffer = ''; |
||||
var cancelled = false; |
||||
|
||||
// What to do when done
|
||||
var onSuccess = function () { |
||||
// Check length
|
||||
if (_this.maxLength && buffer.length > _this.maxLength) { |
||||
cancelled = true; |
||||
winston.warn('document >maxLength', { maxLength: _this.maxLength }); |
||||
response.writeHead(400, { 'content-type': 'application/json' }); |
||||
response.end( |
||||
JSON.stringify({ message: 'Document exceeds maximum length.' }) |
||||
); |
||||
return; |
||||
} |
||||
// And then save if we should
|
||||
_this.chooseKey(function (key) { |
||||
_this.store.set(key, buffer, function (res) { |
||||
if (res) { |
||||
winston.verbose('added document', { key: key }); |
||||
response.writeHead(200, { 'content-type': 'application/json' }); |
||||
response.end(JSON.stringify({ key: key })); |
||||
} |
||||
else { |
||||
winston.verbose('error adding document'); |
||||
response.writeHead(500, { 'content-type': 'application/json' }); |
||||
response.end(JSON.stringify({ message: 'Error adding document.' })); |
||||
} |
||||
}); |
||||
}); |
||||
}; |
||||
|
||||
// If we should, parse a form to grab the data
|
||||
var ct = request.headers['content-type']; |
||||
if (ct && ct.split(';')[0] === 'multipart/form-data') { |
||||
var busboy = new Busboy({ headers: request.headers }); |
||||
busboy.on('field', function (fieldname, val) { |
||||
if (fieldname === 'data') { |
||||
buffer = val; |
||||
} |
||||
}); |
||||
busboy.on('finish', function () { |
||||
onSuccess(); |
||||
}); |
||||
request.pipe(busboy); |
||||
// Otherwise, use our own and just grab flat data from POST body
|
||||
} else { |
||||
request.on('data', function (data) { |
||||
buffer += data.toString(); |
||||
}); |
||||
request.on('end', function () { |
||||
if (cancelled) { return; } |
||||
onSuccess(); |
||||
}); |
||||
request.on('error', function (error) { |
||||
winston.error('connection error: ' + error.message); |
||||
response.writeHead(500, { 'content-type': 'application/json' }); |
||||
response.end(JSON.stringify({ message: 'Connection error.' })); |
||||
cancelled = true; |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
// Keep choosing keys until one isn't taken
|
||||
DocumentHandler.prototype.chooseKey = function(callback) { |
||||
var key = this.acceptableKey(); |
||||
var _this = this; |
||||
this.store.get(key, function(ret) { |
||||
if (ret) { |
||||
_this.chooseKey(callback); |
||||
} else { |
||||
callback(key); |
||||
} |
||||
}, true); // Don't bump expirations when key searching
|
||||
}; |
||||
|
||||
DocumentHandler.prototype.acceptableKey = function() { |
||||
return this.keyGenerator.createKey(this.keyLength); |
||||
}; |
||||
|
||||
module.exports = DocumentHandler; |
@ -0,0 +1,56 @@ |
||||
/*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; |
@ -0,0 +1,63 @@ |
||||
var fs = require('fs'); |
||||
var crypto = require('crypto'); |
||||
|
||||
var winston = require('winston'); |
||||
|
||||
// For storing in files
|
||||
// options[type] = file
|
||||
// options[path] - Where to store
|
||||
|
||||
var FileDocumentStore = function(options) { |
||||
this.basePath = options.path || './data'; |
||||
this.expire = options.expire; |
||||
}; |
||||
|
||||
// Generate md5 of a string
|
||||
FileDocumentStore.md5 = function(str) { |
||||
var md5sum = crypto.createHash('md5'); |
||||
md5sum.update(str); |
||||
return md5sum.digest('hex'); |
||||
}; |
||||
|
||||
// Save data in a file, key as md5 - since we don't know what we could
|
||||
// be passed here
|
||||
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); |
||||
fs.writeFile(fn, data, 'utf8', function(err) { |
||||
if (err) { |
||||
callback(false); |
||||
} |
||||
else { |
||||
callback(true); |
||||
if (_this.expire && !skipExpire) { |
||||
winston.warn('file store cannot set expirations on keys'); |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
} catch(err) { |
||||
callback(false); |
||||
} |
||||
}; |
||||
|
||||
// Get data from a file from key
|
||||
FileDocumentStore.prototype.get = function(key, callback, skipExpire) { |
||||
var _this = this; |
||||
var fn = this.basePath + '/' + FileDocumentStore.md5(key); |
||||
fs.readFile(fn, 'utf8', function(err, data) { |
||||
if (err) { |
||||
callback(false); |
||||
} |
||||
else { |
||||
callback(data); |
||||
if (_this.expire && !skipExpire) { |
||||
winston.warn('file store cannot set expirations on keys'); |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
module.exports = FileDocumentStore; |
@ -0,0 +1,89 @@ |
||||
/*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; |
@ -0,0 +1,54 @@ |
||||
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; |
@ -0,0 +1,88 @@ |
||||
|
||||
|
||||
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; |
@ -0,0 +1,80 @@ |
||||
/*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; |
@ -0,0 +1,89 @@ |
||||
var redis = require('redis'); |
||||
var winston = require('winston'); |
||||
|
||||
// For storing in redis
|
||||
// options[type] = redis
|
||||
// options[host] - The host to connect to (default localhost)
|
||||
// options[port] - The port to connect to (default 5379)
|
||||
// options[db] - The db to use (default 0)
|
||||
// options[expire] - The time to live for each key set (default never)
|
||||
|
||||
var RedisDocumentStore = function(options, client) { |
||||
this.expire = options.expire; |
||||
if (client) { |
||||
winston.info('using predefined redis client'); |
||||
RedisDocumentStore.client = client; |
||||
} else if (!RedisDocumentStore.client) { |
||||
winston.info('configuring redis'); |
||||
RedisDocumentStore.connect(options); |
||||
} |
||||
}; |
||||
|
||||
// Create a connection according to config
|
||||
RedisDocumentStore.connect = function(options) { |
||||
var host = options.host || '127.0.0.1'; |
||||
var port = options.port || 6379; |
||||
var index = options.db || 0; |
||||
RedisDocumentStore.client = redis.createClient(port, host); |
||||
// authenticate if password is provided
|
||||
if (options.password) { |
||||
RedisDocumentStore.client.auth(options.password); |
||||
} |
||||
|
||||
RedisDocumentStore.client.on('error', function(err) { |
||||
winston.error('redis disconnected', err); |
||||
}); |
||||
|
||||
RedisDocumentStore.client.select(index, function(err) { |
||||
if (err) { |
||||
winston.error( |
||||
'error connecting to redis index ' + index, |
||||
{ error: err } |
||||
); |
||||
process.exit(1); |
||||
} |
||||
else { |
||||
winston.info('connected to redis on ' + host + ':' + port + '/' + index); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
// Save file in a key
|
||||
RedisDocumentStore.prototype.set = function(key, data, callback, skipExpire) { |
||||
var _this = this; |
||||
RedisDocumentStore.client.set(key, data, function(err) { |
||||
if (err) { |
||||
callback(false); |
||||
} |
||||
else { |
||||
if (!skipExpire) { |
||||
_this.setExpiration(key); |
||||
} |
||||
callback(true); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
// Expire a key in expire time if set
|
||||
RedisDocumentStore.prototype.setExpiration = function(key) { |
||||
if (this.expire) { |
||||
RedisDocumentStore.client.expire(key, this.expire, function(err) { |
||||
if (err) { |
||||
winston.error('failed to set expiry on key: ' + key); |
||||
} |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
// Get a file from a key
|
||||
RedisDocumentStore.prototype.get = function(key, callback, skipExpire) { |
||||
var _this = this; |
||||
RedisDocumentStore.client.get(key, function(err, reply) { |
||||
if (!err && !skipExpire) { |
||||
_this.setExpiration(key); |
||||
} |
||||
callback(err ? false : reply); |
||||
}); |
||||
}; |
||||
|
||||
module.exports = RedisDocumentStore; |
@ -0,0 +1,46 @@ |
||||
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; |
@ -0,0 +1,32 @@ |
||||
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; |
||||
} |
||||
|
||||
}; |
@ -0,0 +1,27 @@ |
||||
// Draws inspiration from pwgen and http://tools.arantius.com/password
|
||||
|
||||
const randOf = (collection) => { |
||||
return () => { |
||||
return collection[Math.floor(Math.random() * collection.length)]; |
||||
}; |
||||
}; |
||||
|
||||
// Helper methods to get an random vowel or consonant
|
||||
const randVowel = randOf('aeiou'); |
||||
const randConsonant = randOf('bcdfghjklmnpqrstvwxyz'); |
||||
|
||||
module.exports = class PhoneticKeyGenerator { |
||||
|
||||
// Generate a phonetic key of alternating consonant & vowel
|
||||
createKey(keyLength) { |
||||
let text = ''; |
||||
const start = Math.round(Math.random()); |
||||
|
||||
for (let i = 0; i < keyLength; i++) { |
||||
text += (i % 2 == start) ? randConsonant() : randVowel(); |
||||
} |
||||
|
||||
return text; |
||||
} |
||||
|
||||
}; |
@ -0,0 +1,20 @@ |
||||
module.exports = class RandomKeyGenerator { |
||||
|
||||
// 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; |
||||
} |
||||
|
||||
}; |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,47 @@ |
||||
{ |
||||
"name": "haste", |
||||
"version": "0.1.0", |
||||
"private": true, |
||||
"description": "Private Pastebin Server", |
||||
"keywords": [ |
||||
"paste", |
||||
"pastebin" |
||||
], |
||||
"author": { |
||||
"name": "John Crepezzi", |
||||
"email": "john.crepezzi@gmail.com", |
||||
"url": "http://seejohncode.com/" |
||||
}, |
||||
"main": "haste", |
||||
"dependencies": { |
||||
"busboy": "0.2.4", |
||||
"connect": "^3.7.0", |
||||
"connect-ratelimit": "0.0.7", |
||||
"connect-route": "0.1.5", |
||||
"pg": "^8.0.0", |
||||
"redis": "0.8.1", |
||||
"redis-url": "0.1.0", |
||||
"st": "^2.0.0", |
||||
"uglify-js": "3.1.6", |
||||
"winston": "^2.0.0" |
||||
}, |
||||
"devDependencies": { |
||||
"mocha": "^8.1.3" |
||||
}, |
||||
"bundledDependencies": [], |
||||
"bin": { |
||||
"haste-server": "./server.js" |
||||
}, |
||||
"files": [ |
||||
"server.js", |
||||
"lib", |
||||
"static" |
||||
], |
||||
"directories": { |
||||
"lib": "./lib" |
||||
}, |
||||
"scripts": { |
||||
"start": "node server.js", |
||||
"test": "mocha --recursive" |
||||
} |
||||
} |
@ -1 +0,0 @@ |
||||
{"name":"Haste-server","body":"# Pastebin\r\n\r\nhaste-server is an open source pastebin. You can see an example at [hastebin.com](http://hastebin.com), and it's easy to install locally.\r\n\r\n## GitHub\r\n\r\nCheck out the project on [github](https://github.com/seejohnrun/haste-server)","tagline":"open source pastebin written in node.js","google":"","note":"Don't delete this file! It's used internally to help with page regeneration."} |
@ -0,0 +1,164 @@ |
||||
var http = require('http'); |
||||
var fs = require('fs'); |
||||
|
||||
var uglify = require('uglify-js'); |
||||
var winston = require('winston'); |
||||
var connect = require('connect'); |
||||
var route = require('connect-route'); |
||||
var connect_st = require('st'); |
||||
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 config = JSON.parse(fs.readFileSync(configPath, 'utf8')); |
||||
config.port = process.env.PORT || config.port || 7777; |
||||
config.host = process.env.HOST || config.host || 'localhost'; |
||||
|
||||
// Set up the logger
|
||||
if (config.logging) { |
||||
try { |
||||
winston.remove(winston.transports.Console); |
||||
} catch(e) { |
||||
/* was not present */ |
||||
} |
||||
|
||||
var detail, type; |
||||
for (var i = 0; i < config.logging.length; i++) { |
||||
detail = config.logging[i]; |
||||
type = detail.type; |
||||
delete detail.type; |
||||
winston.add(winston.transports[type], detail); |
||||
} |
||||
} |
||||
|
||||
// build the store from the config on-demand - so that we don't load it
|
||||
// for statics
|
||||
if (!config.storage) { |
||||
config.storage = { type: 'file' }; |
||||
} |
||||
if (!config.storage.type) { |
||||
config.storage.type = 'file'; |
||||
} |
||||
|
||||
var Store, preferredStore; |
||||
|
||||
if (process.env.REDISTOGO_URL && config.storage.type === 'redis') { |
||||
var redisClient = require('redis-url').connect(process.env.REDISTOGO_URL); |
||||
Store = require('./lib/document_stores/redis'); |
||||
preferredStore = new Store(config.storage, redisClient); |
||||
} |
||||
else { |
||||
Store = require('./lib/document_stores/' + config.storage.type); |
||||
preferredStore = new Store(config.storage); |
||||
} |
||||
|
||||
// Compress the static javascript assets
|
||||
if (config.recompressStaticAssets) { |
||||
var list = fs.readdirSync('./static'); |
||||
for (var j = 0; j < list.length; j++) { |
||||
var item = list[j]; |
||||
if ((item.indexOf('.js') === item.length - 3) && (item.indexOf('.min.js') === -1)) { |
||||
var dest = item.substring(0, item.length - 3) + '.min' + item.substring(item.length - 3); |
||||
var orig_code = fs.readFileSync('./static/' + item, 'utf8'); |
||||
|
||||
fs.writeFileSync('./static/' + dest, uglify.minify(orig_code).code, 'utf8'); |
||||
winston.info('compressed ' + item + ' into ' + dest); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Send the static documents into the preferred store, skipping expirations
|
||||
var path, data; |
||||
for (var name in config.documents) { |
||||
path = config.documents[name]; |
||||
data = fs.readFileSync(path, 'utf8'); |
||||
winston.info('loading static document', { name: name, path: path }); |
||||
if (data) { |
||||
preferredStore.set(name, data, function(cb) { |
||||
winston.debug('loaded static document', { success: cb }); |
||||
}, true); |
||||
} |
||||
else { |
||||
winston.warn('failed to load static document', { name: name, path: path }); |
||||
} |
||||
} |
||||
|
||||
// Pick up a key generator
|
||||
var pwOptions = config.keyGenerator || {}; |
||||
pwOptions.type = pwOptions.type || 'random'; |
||||
var gen = require('./lib/key_generators/' + pwOptions.type); |
||||
var keyGenerator = new gen(pwOptions); |
||||
|
||||
// Configure the document handler
|
||||
var documentHandler = new DocumentHandler({ |
||||
store: preferredStore, |
||||
maxLength: config.maxLength, |
||||
keyLength: config.keyLength, |
||||
keyGenerator: keyGenerator |
||||
}); |
||||
|
||||
var app = connect(); |
||||
|
||||
// Rate limit all requests
|
||||
if (config.rateLimits) { |
||||
config.rateLimits.end = true; |
||||
app.use(connect_rate_limit(config.rateLimits)); |
||||
} |
||||
|
||||
// first look at API calls
|
||||
app.use(route(function(router) { |
||||
// get raw documents - support getting with extension
|
||||
|
||||
router.get('/raw/:id', function(request, response) { |
||||
return documentHandler.handleRawGet(request, response, config); |
||||
}); |
||||
|
||||
router.head('/raw/:id', function(request, response) { |
||||
return documentHandler.handleRawGet(request, response, config); |
||||
}); |
||||
|
||||
// add documents
|
||||
|
||||
router.post('/documents', function(request, response) { |
||||
return documentHandler.handlePost(request, response); |
||||
}); |
||||
|
||||
// get documents
|
||||
router.get('/documents/:id', function(request, response) { |
||||
return documentHandler.handleGet(request, response, config); |
||||
}); |
||||
|
||||
router.head('/documents/:id', function(request, response) { |
||||
return documentHandler.handleGet(request, response, config); |
||||
}); |
||||
})); |
||||
|
||||
// Otherwise, try to match static files
|
||||
app.use(connect_st({ |
||||
path: __dirname + '/static', |
||||
content: { maxAge: config.staticMaxAge }, |
||||
passthrough: true, |
||||
index: false |
||||
})); |
||||
|
||||
// Then we can loop back - and everything else should be a token,
|
||||
// so route it back to /
|
||||
app.use(route(function(router) { |
||||
router.get('/:id', function(request, response, next) { |
||||
request.sturl = '/'; |
||||
next(); |
||||
}); |
||||
})); |
||||
|
||||
// And match index
|
||||
app.use(connect_st({ |
||||
path: __dirname + '/static', |
||||
content: { maxAge: config.staticMaxAge }, |
||||
index: 'index.html' |
||||
})); |
||||
|
||||
http.createServer(app).listen(config.port, config.host); |
||||
|
||||
winston.info('listening on ' + config.host + ':' + config.port); |
@ -0,0 +1,175 @@ |
||||
body { |
||||
background: #002B36; |
||||
padding: 20px 50px; |
||||
margin: 0px; |
||||
} |
||||
|
||||
/* textarea */ |
||||
|
||||
textarea { |
||||
background: transparent; |
||||
border: 0px; |
||||
color: #fff; |
||||
padding: 0px; |
||||
width: 100%; |
||||
height: 100%; |
||||
font-family: monospace; |
||||
outline: none; |
||||
resize: none; |
||||
font-size: 13px; |
||||
margin-top: 0; |
||||
margin-bottom: 0; |
||||
} |
||||
|
||||
/* the line numbers */ |
||||
|
||||
#linenos { |
||||
color: #7d7d7d; |
||||
z-index: -1000; |
||||
position: absolute; |
||||
top: 20px; |
||||
left: 0px; |
||||
width: 30px; /* 30 to get 20 away from box */ |
||||
font-size: 13px; |
||||
font-family: monospace; |
||||
text-align: right; |
||||
user-select: none; |
||||
} |
||||
|
||||
/* code box when locked */ |
||||
|
||||
#box { |
||||
padding: 0px; |
||||
margin: 0px; |
||||
width: 100%; |
||||
border: 0px; |
||||
outline: none; |
||||
font-size: 13px; |
||||
overflow: inherit; |
||||
} |
||||
|
||||
#box code { |
||||
padding: 0px; |
||||
background: transparent !important; /* don't hide hastebox */ |
||||
} |
||||
|
||||
/* key */ |
||||
|
||||
#key { |
||||
position: fixed; |
||||
top: 0px; |
||||
right: 0px; |
||||
z-index: +1000; /* watch out */ |
||||
} |
||||
|
||||
#box1 { |
||||
padding: 5px; |
||||
text-align: center; |
||||
background: #00222b; |
||||
} |
||||
|
||||
#box2 { |
||||
background: #08323c; |
||||
font-size: 0px; |
||||
padding: 0px 5px; |
||||
} |
||||
|
||||
#box1 a.logo, #box1 a.logo:visited { |
||||
display: inline-block; |
||||
background: url(logo.png); |
||||
width: 126px; |
||||
height: 42px; |
||||
} |
||||
|
||||
#box1 a.logo:hover { |
||||
background-position: 0 bottom; |
||||
} |
||||
|
||||
#box2 .function { |
||||
background: url(function-icons.png); |
||||
width: 32px; |
||||
height: 37px; |
||||
display: inline-block; |
||||
position: relative; |
||||
} |
||||
|
||||
#box2 .link embed { |
||||
vertical-align: bottom; /* fix for zeroClipboard style */ |
||||
} |
||||
|
||||
#box2 .function.enabled:hover { |
||||
cursor: hand; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
#pointer { |
||||
display: block; |
||||
height: 5px; |
||||
width: 10px; |
||||
background: url(hover-dropdown-tip.png); |
||||
bottom: 0px; |
||||
position: absolute; |
||||
margin: auto; |
||||
left: 0px; |
||||
right: 0px; |
||||
} |
||||
|
||||
#box3, #messages li { |
||||
background: #173e48; |
||||
font-family: Helvetica, sans-serif; |
||||
font-size: 12px; |
||||
line-height: 14px; |
||||
padding: 10px 15px; |
||||
user-select: none; |
||||
} |
||||
|
||||
#box3 .label, #messages li { |
||||
color: #fff; |
||||
font-weight: bold; |
||||
} |
||||
|
||||
#box3 .shortcut { |
||||
color: #c4dce3; |
||||
font-weight: normal; |
||||
} |
||||
|
||||
#box2 .function.save { background-position: -5px top; } |
||||
#box2 .function.enabled.save { background-position: -5px center; } |
||||
#box2 .function.enabled.save:hover { background-position: -5px bottom; } |
||||
|
||||
#box2 .function.new { background-position: -42px top; } |
||||
#box2 .function.enabled.new { background-position: -42px center; } |
||||
#box2 .function.enabled.new:hover { background-position: -42px bottom; } |
||||
|
||||
#box2 .function.duplicate { background-position: -79px top; } |
||||
#box2 .function.enabled.duplicate { background-position: -79px center; } |
||||
#box2 .function.enabled.duplicate:hover { background-position: -79px bottom; } |
||||
|
||||
#box2 .function.raw { background-position: -116px top; } |
||||
#box2 .function.enabled.raw { background-position: -116px center; } |
||||
#box2 .function.enabled.raw:hover { background-position: -116px bottom; } |
||||
|
||||
#box2 .function.twitter { background-position: -153px top; } |
||||
#box2 .function.enabled.twitter { background-position: -153px center; } |
||||
#box2 .function.enabled.twitter:hover { background-position: -153px bottom; } |
||||
#box2 .button-picture{ border-width: 0; font-size: inherit; } |
||||
|
||||
#messages { |
||||
position:fixed; |
||||
top:0px; |
||||
right:138px; |
||||
margin:0; |
||||
padding:0; |
||||
width:400px; |
||||
} |
||||
|
||||
#messages li { |
||||
background:rgba(23,62,72,0.8); |
||||
margin:0 auto; |
||||
list-style:none; |
||||
} |
||||
|
||||
#messages li.error { |
||||
background:rgba(102,8,0,0.8); |
||||
} |
||||
|
@ -0,0 +1,398 @@ |
||||
/* global $, hljs, window, document */ |
||||
|
||||
///// represents a single document
|
||||
|
||||
var haste_document = function() { |
||||
this.locked = false; |
||||
}; |
||||
|
||||
// Escapes HTML tag characters
|
||||
haste_document.prototype.htmlEscape = function(s) { |
||||
return s |
||||
.replace(/&/g, '&') |
||||
.replace(/>/g, '>') |
||||
.replace(/</g, '<') |
||||
.replace(/"/g, '"'); |
||||
}; |
||||
|
||||
// Get this document from the server and lock it here
|
||||
haste_document.prototype.load = function(key, callback, lang) { |
||||
var _this = this; |
||||
$.ajax('/documents/' + key, { |
||||
type: 'get', |
||||
dataType: 'json', |
||||
success: function(res) { |
||||
_this.locked = true; |
||||
_this.key = key; |
||||
_this.data = res.data; |
||||
try { |
||||
var high; |
||||
if (lang === 'txt') { |
||||
high = { value: _this.htmlEscape(res.data) }; |
||||
} |
||||
else if (lang) { |
||||
high = hljs.highlight(lang, res.data); |
||||
} |
||||
else { |
||||
high = hljs.highlightAuto(res.data); |
||||
} |
||||
} catch(err) { |
||||
// failed highlight, fall back on auto
|
||||
high = hljs.highlightAuto(res.data); |
||||
} |
||||
callback({ |
||||
value: high.value, |
||||
key: key, |
||||
language: high.language || lang, |
||||
lineCount: res.data.split('\n').length |
||||
}); |
||||
}, |
||||
error: function() { |
||||
callback(false); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
// Save this document to the server and lock it here
|
||||
haste_document.prototype.save = function(data, callback) { |
||||
if (this.locked) { |
||||
return false; |
||||
} |
||||
this.data = data; |
||||
var _this = this; |
||||
$.ajax('/documents', { |
||||
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, { |
||||
value: high.value, |
||||
key: res.key, |
||||
language: high.language, |
||||
lineCount: data.split('\n').length |
||||
}); |
||||
}, |
||||
error: function(res) { |
||||
try { |
||||
callback($.parseJSON(res.responseText)); |
||||
} |
||||
catch (e) { |
||||
callback({message: 'Something went wrong!'}); |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
///// represents the paste application
|
||||
|
||||
var haste = function(appName, options) { |
||||
this.appName = appName; |
||||
this.$textarea = $('textarea'); |
||||
this.$box = $('#box'); |
||||
this.$code = $('#box code'); |
||||
this.$linenos = $('#linenos'); |
||||
this.options = options; |
||||
this.configureShortcuts(); |
||||
this.configureButtons(); |
||||
// If twitter is disabled, hide the button
|
||||
if (!options.twitter) { |
||||
$('#box2 .twitter').hide(); |
||||
} |
||||
}; |
||||
|
||||
// Set the page title - include the appName
|
||||
haste.prototype.setTitle = function(ext) { |
||||
var title = ext ? this.appName + ' - ' + ext : this.appName; |
||||
document.title = title; |
||||
}; |
||||
|
||||
// Show a message box
|
||||
haste.prototype.showMessage = function(msg, cls) { |
||||
var msgBox = $('<li class="'+(cls || 'info')+'">'+msg+'</li>'); |
||||
$('#messages').prepend(msgBox); |
||||
setTimeout(function() { |
||||
msgBox.slideUp('fast', function() { $(this).remove(); }); |
||||
}, 3000); |
||||
}; |
||||
|
||||
// Show the light key
|
||||
haste.prototype.lightKey = function() { |
||||
this.configureKey(['new', 'save']); |
||||
}; |
||||
|
||||
// Show the full key
|
||||
haste.prototype.fullKey = function() { |
||||
this.configureKey(['new', 'duplicate', 'twitter', 'raw']); |
||||
}; |
||||
|
||||
// Set the key up for certain things to be enabled
|
||||
haste.prototype.configureKey = function(enable) { |
||||
var $this, i = 0; |
||||
$('#box2 .function').each(function() { |
||||
$this = $(this); |
||||
for (i = 0; i < enable.length; i++) { |
||||
if ($this.hasClass(enable[i])) { |
||||
$this.addClass('enabled'); |
||||
return true; |
||||
} |
||||
} |
||||
$this.removeClass('enabled'); |
||||
}); |
||||
}; |
||||
|
||||
// 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, '/'); |
||||
} |
||||
this.setTitle(); |
||||
this.lightKey(); |
||||
this.$textarea.val('').show('fast', function() { |
||||
this.focus(); |
||||
}); |
||||
this.removeLineNumbers(); |
||||
}; |
||||
|
||||
// Map of common extensions
|
||||
// Note: this list does not need to include anything that IS its extension,
|
||||
// due to the behavior of lookupTypeByExtension and lookupExtensionByType
|
||||
// Note: optimized for lookupTypeByExtension
|
||||
haste.extensionMap = { |
||||
rb: 'ruby', py: 'python', pl: 'perl', php: 'php', scala: 'scala', go: 'go', |
||||
xml: 'xml', html: 'xml', htm: 'xml', css: 'css', js: 'javascript', vbs: 'vbscript', |
||||
lua: 'lua', pas: 'delphi', java: 'java', cpp: 'cpp', cc: 'cpp', m: 'objectivec', |
||||
vala: 'vala', sql: 'sql', sm: 'smalltalk', lisp: 'lisp', ini: 'ini', |
||||
diff: 'diff', bash: 'bash', sh: 'bash', tex: 'tex', erl: 'erlang', hs: 'haskell', |
||||
md: 'markdown', txt: '', coffee: 'coffee', swift: 'swift' |
||||
}; |
||||
|
||||
// Look up the extension preferred for a type
|
||||
// If not found, return the type itself - which we'll place as the extension
|
||||
haste.prototype.lookupExtensionByType = function(type) { |
||||
for (var key in haste.extensionMap) { |
||||
if (haste.extensionMap[key] === type) return key; |
||||
} |
||||
return type; |
||||
}; |
||||
|
||||
// Look up the type for a given extension
|
||||
// If not found, return the extension - which we'll attempt to use as the type
|
||||
haste.prototype.lookupTypeByExtension = function(ext) { |
||||
return haste.extensionMap[ext] || ext; |
||||
}; |
||||
|
||||
// Add line numbers to the document
|
||||
// For the specified number of lines
|
||||
haste.prototype.addLineNumbers = function(lineCount) { |
||||
var h = ''; |
||||
for (var i = 0; i < lineCount; i++) { |
||||
h += (i + 1).toString() + '<br/>'; |
||||
} |
||||
$('#linenos').html(h); |
||||
}; |
||||
|
||||
// Remove the line numbers
|
||||
haste.prototype.removeLineNumbers = function() { |
||||
$('#linenos').html('>'); |
||||
}; |
||||
|
||||
// Load a document and show it
|
||||
haste.prototype.loadDocument = function(key) { |
||||
// Split the key up
|
||||
var parts = key.split('.', 2); |
||||
// Ask for what we want
|
||||
var _this = this; |
||||
_this.doc = new haste_document(); |
||||
_this.doc.load(parts[0], function(ret) { |
||||
if (ret) { |
||||
_this.$code.html(ret.value); |
||||
_this.setTitle(ret.key); |
||||
_this.fullKey(); |
||||
_this.$textarea.val('').hide(); |
||||
_this.$box.show().focus(); |
||||
_this.addLineNumbers(ret.lineCount); |
||||
} |
||||
else { |
||||
_this.newDocument(); |
||||
} |
||||
}, this.lookupTypeByExtension(parts[1])); |
||||
}; |
||||
|
||||
// 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); |
||||
} |
||||
}; |
||||
|
||||
// Lock the current document
|
||||
haste.prototype.lockDocument = function() { |
||||
var _this = this; |
||||
this.doc.save(this.$textarea.val(), function(err, ret) { |
||||
if (err) { |
||||
_this.showMessage(err.message, 'error'); |
||||
} |
||||
else if (ret) { |
||||
_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(); |
||||
_this.addLineNumbers(ret.lineCount); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
haste.prototype.configureButtons = function() { |
||||
var _this = this; |
||||
this.buttons = [ |
||||
{ |
||||
$where: $('#box2 .save'), |
||||
label: 'Save', |
||||
shortcutDescription: 'control + s', |
||||
shortcut: function(evt) { |
||||
return evt.ctrlKey && (evt.keyCode === 83); |
||||
}, |
||||
action: function() { |
||||
if (_this.$textarea.val().replace(/^\s+|\s+$/g, '') !== '') { |
||||
_this.lockDocument(); |
||||
} |
||||
} |
||||
}, |
||||
{ |
||||
$where: $('#box2 .new'), |
||||
label: 'New', |
||||
shortcut: function(evt) { |
||||
return evt.ctrlKey && evt.keyCode === 78; |
||||
}, |
||||
shortcutDescription: 'control + n', |
||||
action: function() { |
||||
_this.newDocument(!_this.doc.key); |
||||
} |
||||
}, |
||||
{ |
||||
$where: $('#box2 .duplicate'), |
||||
label: 'Duplicate & Edit', |
||||
shortcut: function(evt) { |
||||
return _this.doc.locked && evt.ctrlKey && evt.keyCode === 68; |
||||
}, |
||||
shortcutDescription: 'control + d', |
||||
action: function() { |
||||
_this.duplicateDocument(); |
||||
} |
||||
}, |
||||
{ |
||||
$where: $('#box2 .raw'), |
||||
label: 'Just Text', |
||||
shortcut: function(evt) { |
||||
return evt.ctrlKey && evt.shiftKey && evt.keyCode === 82; |
||||
}, |
||||
shortcutDescription: 'control + shift + r', |
||||
action: function() { |
||||
window.location.href = '/raw/' + _this.doc.key; |
||||
} |
||||
}, |
||||
{ |
||||
$where: $('#box2 .twitter'), |
||||
label: 'Twitter', |
||||
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)); |
||||
} |
||||
} |
||||
]; |
||||
for (var i = 0; i < this.buttons.length; i++) { |
||||
this.configureButton(this.buttons[i]); |
||||
} |
||||
}; |
||||
|
||||
haste.prototype.configureButton = function(options) { |
||||
// Handle the click action
|
||||
options.$where.click(function(evt) { |
||||
evt.preventDefault(); |
||||
if (!options.clickDisabled && $(this).hasClass('enabled')) { |
||||
options.action(); |
||||
} |
||||
}); |
||||
// Show the label
|
||||
options.$where.mouseenter(function() { |
||||
$('#box3 .label').text(options.label); |
||||
$('#box3 .shortcut').text(options.shortcutDescription || ''); |
||||
$('#box3').show(); |
||||
$(this).append($('#pointer').remove().show()); |
||||
}); |
||||
// Hide the label
|
||||
options.$where.mouseleave(function() { |
||||
$('#box3').hide(); |
||||
$('#pointer').hide(); |
||||
}); |
||||
}; |
||||
|
||||
// Configure keyboard shortcuts for the textarea
|
||||
haste.prototype.configureShortcuts = function() { |
||||
var _this = this; |
||||
$(document.body).keydown(function(evt) { |
||||
var button; |
||||
for (var i = 0 ; i < _this.buttons.length; i++) { |
||||
button = _this.buttons[i]; |
||||
if (button.shortcut && button.shortcut(evt)) { |
||||
evt.preventDefault(); |
||||
button.action(); |
||||
return; |
||||
} |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
///// Tab behavior in the textarea - 2 spaces per tab
|
||||
$(function() { |
||||
|
||||
$('textarea').keydown(function(evt) { |
||||
if (evt.keyCode === 9) { |
||||
evt.preventDefault(); |
||||
var myValue = ' '; |
||||
// http://stackoverflow.com/questions/946534/insert-text-into-textarea-with-jquery
|
||||
// For browsers like Internet Explorer
|
||||
if (document.selection) { |
||||
this.focus(); |
||||
var sel = document.selection.createRange(); |
||||
sel.text = myValue; |
||||
this.focus(); |
||||
} |
||||
// Mozilla and Webkit
|
||||
else if (this.selectionStart || this.selectionStart == '0') { |
||||
var startPos = this.selectionStart; |
||||
var endPos = this.selectionEnd; |
||||
var scrollTop = this.scrollTop; |
||||
this.value = this.value.substring(0, startPos) + myValue + |
||||
this.value.substring(endPos,this.value.length); |
||||
this.focus(); |
||||
this.selectionStart = startPos + myValue.length; |
||||
this.selectionEnd = startPos + myValue.length; |
||||
this.scrollTop = scrollTop; |
||||
} |
||||
else { |
||||
this.value += myValue; |
||||
this.focus(); |
||||
} |
||||
} |
||||
}); |
||||
|
||||
}); |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 6.1 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 2.8 KiB |
@ -0,0 +1,68 @@ |
||||
<html> |
||||
|
||||
<head> |
||||
|
||||
<title>hastebin</title> |
||||
<meta charset="utf-8" /> |
||||
<link rel="stylesheet" type="text/css" href="solarized_dark.css"/> |
||||
<link rel="stylesheet" type="text/css" href="application.css"/> |
||||
|
||||
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> |
||||
<script type="text/javascript" src="highlight.min.js"></script> |
||||
<script type="text/javascript" src="application.min.js"></script> |
||||
|
||||
<meta name="robots" content="noindex,nofollow"/> |
||||
|
||||
<script type="text/javascript"> |
||||
var app = null; |
||||
// Handle pops |
||||
var handlePop = function(evt) { |
||||
var path = evt.target.location.pathname; |
||||
if (path === '/') { app.newDocument(true); } |
||||
else { app.loadDocument(path.substring(1, path.length)); } |
||||
}; |
||||
// Set up the pop state to handle loads, skipping the first load |
||||
// to make chrome behave like others: |
||||
// http://code.google.com/p/chromium/issues/detail?id=63040 |
||||
setTimeout(function() { |
||||
window.onpopstate = function(evt) { |
||||
try { handlePop(evt); } catch(err) { /* not loaded yet */ } |
||||
}; |
||||
}, 1000); |
||||
// Construct app and load initial path |
||||
$(function() { |
||||
app = new haste('hastebin', { twitter: true }); |
||||
handlePop({ target: window }); |
||||
}); |
||||
</script> |
||||
|
||||
</head> |
||||
|
||||
<body> |
||||
<ul id="messages"></ul> |
||||
|
||||
<div id="key"> |
||||
<div id="pointer" style="display:none;"></div> |
||||
<div id="box1"> |
||||
<a href="/about.md" class="logo"></a> |
||||
</div> |
||||
<div id="box2"> |
||||
<button class="save function button-picture">Save</button> |
||||
<button class="new function button-picture">New</button> |
||||
<button class="duplicate function button-picture">Duplicate & Edit</button> |
||||
<button class="raw function button-picture">Just Text</button> |
||||
<button class="twitter function button-picture">Twitter</button> |
||||
</div> |
||||
<div id="box3" style="display:none;"> |
||||
<div class="label"></div> |
||||
<div class="shortcut"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div id="linenos"></div> |
||||
<pre id="box" style="display:none;" class="hljs" tabindex="0"><code></code></pre> |
||||
<textarea spellcheck="false" style="display:none;"></textarea> |
||||
|
||||
</body> |
||||
|
||||
</html> |
After Width: | Height: | Size: 4.6 KiB |
@ -0,0 +1,4 @@ |
||||
User-agent: * |
||||
Disallow: /* |
||||
Allow: /?okparam= |
||||
Allow: /$ |
@ -0,0 +1,84 @@ |
||||
/* |
||||
|
||||
Orginal Style from ethanschoonover.com/solarized (c) Jeremy Hull <sourdrums@gmail.com> |
||||
|
||||
*/ |
||||
|
||||
.hljs { |
||||
display: block; |
||||
overflow-x: auto; |
||||
padding: 0.5em; |
||||
background: #002b36; |
||||
color: #839496; |
||||
} |
||||
|
||||
.hljs-comment, |
||||
.hljs-quote { |
||||
color: #586e75; |
||||
} |
||||
|
||||
/* Solarized Green */ |
||||
.hljs-keyword, |
||||
.hljs-selector-tag, |
||||
.hljs-addition { |
||||
color: #859900; |
||||
} |
||||
|
||||
/* Solarized Cyan */ |
||||
.hljs-number, |
||||
.hljs-string, |
||||
.hljs-meta .hljs-meta-string, |
||||
.hljs-literal, |
||||
.hljs-doctag, |
||||
.hljs-regexp { |
||||
color: #2aa198; |
||||
} |
||||
|
||||
/* Solarized Blue */ |
||||
.hljs-title, |
||||
.hljs-section, |
||||
.hljs-name, |
||||
.hljs-selector-id, |
||||
.hljs-selector-class { |
||||
color: #268bd2; |
||||
} |
||||
|
||||
/* Solarized Yellow */ |
||||
.hljs-attribute, |
||||
.hljs-attr, |
||||
.hljs-variable, |
||||
.hljs-template-variable, |
||||
.hljs-class .hljs-title, |
||||
.hljs-type { |
||||
color: #b58900; |
||||
} |
||||
|
||||
/* Solarized Orange */ |
||||
.hljs-symbol, |
||||
.hljs-bullet, |
||||
.hljs-subst, |
||||
.hljs-meta, |
||||
.hljs-meta .hljs-keyword, |
||||
.hljs-selector-attr, |
||||
.hljs-selector-pseudo, |
||||
.hljs-link { |
||||
color: #cb4b16; |
||||
} |
||||
|
||||
/* Solarized Red */ |
||||
.hljs-built_in, |
||||
.hljs-deletion { |
||||
color: #dc322f; |
||||
} |
||||
|
||||
.hljs-formula { |
||||
background: #073642; |
||||
} |
||||
|
||||
.hljs-emphasis { |
||||
font-style: italic; |
||||
} |
||||
|
||||
.hljs-strong { |
||||
font-weight: bold; |
||||
} |
@ -1,69 +0,0 @@ |
||||
.highlight { background: #ffffff; } |
||||
.highlight .c { color: #999988; font-style: italic } /* Comment */ |
||||
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ |
||||
.highlight .k { font-weight: bold } /* Keyword */ |
||||
.highlight .o { font-weight: bold } /* Operator */ |
||||
.highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ |
||||
.highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ |
||||
.highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ |
||||
.highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ |
||||
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ |
||||
.highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ |
||||
.highlight .ge { font-style: italic } /* Generic.Emph */ |
||||
.highlight .gr { color: #aa0000 } /* Generic.Error */ |
||||
.highlight .gh { color: #999999 } /* Generic.Heading */ |
||||
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ |
||||
.highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ |
||||
.highlight .go { color: #888888 } /* Generic.Output */ |
||||
.highlight .gp { color: #555555 } /* Generic.Prompt */ |
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */ |
||||
.highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */ |
||||
.highlight .gt { color: #aa0000 } /* Generic.Traceback */ |
||||
.highlight .kc { font-weight: bold } /* Keyword.Constant */ |
||||
.highlight .kd { font-weight: bold } /* Keyword.Declaration */ |
||||
.highlight .kn { font-weight: bold } /* Keyword.Namespace */ |
||||
.highlight .kp { font-weight: bold } /* Keyword.Pseudo */ |
||||
.highlight .kr { font-weight: bold } /* Keyword.Reserved */ |
||||
.highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ |
||||
.highlight .m { color: #009999 } /* Literal.Number */ |
||||
.highlight .s { color: #d14 } /* Literal.String */ |
||||
.highlight .na { color: #008080 } /* Name.Attribute */ |
||||
.highlight .nb { color: #0086B3 } /* Name.Builtin */ |
||||
.highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ |
||||
.highlight .no { color: #008080 } /* Name.Constant */ |
||||
.highlight .ni { color: #800080 } /* Name.Entity */ |
||||
.highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ |
||||
.highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ |
||||
.highlight .nn { color: #555555 } /* Name.Namespace */ |
||||
.highlight .nt { color: #000080 } /* Name.Tag */ |
||||
.highlight .nv { color: #008080 } /* Name.Variable */ |
||||
.highlight .ow { font-weight: bold } /* Operator.Word */ |
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */ |
||||
.highlight .mf { color: #009999 } /* Literal.Number.Float */ |
||||
.highlight .mh { color: #009999 } /* Literal.Number.Hex */ |
||||
.highlight .mi { color: #009999 } /* Literal.Number.Integer */ |
||||
.highlight .mo { color: #009999 } /* Literal.Number.Oct */ |
||||
.highlight .sb { color: #d14 } /* Literal.String.Backtick */ |
||||
.highlight .sc { color: #d14 } /* Literal.String.Char */ |
||||
.highlight .sd { color: #d14 } /* Literal.String.Doc */ |
||||
.highlight .s2 { color: #d14 } /* Literal.String.Double */ |
||||
.highlight .se { color: #d14 } /* Literal.String.Escape */ |
||||
.highlight .sh { color: #d14 } /* Literal.String.Heredoc */ |
||||
.highlight .si { color: #d14 } /* Literal.String.Interpol */ |
||||
.highlight .sx { color: #d14 } /* Literal.String.Other */ |
||||
.highlight .sr { color: #009926 } /* Literal.String.Regex */ |
||||
.highlight .s1 { color: #d14 } /* Literal.String.Single */ |
||||
.highlight .ss { color: #990073 } /* Literal.String.Symbol */ |
||||
.highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ |
||||
.highlight .vc { color: #008080 } /* Name.Variable.Class */ |
||||
.highlight .vg { color: #008080 } /* Name.Variable.Global */ |
||||
.highlight .vi { color: #008080 } /* Name.Variable.Instance */ |
||||
.highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ |
||||
|
||||
.type-csharp .highlight .k { color: #0000FF } |
||||
.type-csharp .highlight .kt { color: #0000FF } |
||||
.type-csharp .highlight .nf { color: #000000; font-weight: normal } |
||||
.type-csharp .highlight .nc { color: #2B91AF } |
||||
.type-csharp .highlight .nn { color: #000000 } |
||||
.type-csharp .highlight .s { color: #A31515 } |
||||
.type-csharp .highlight .sc { color: #A31515 } |
@ -1,255 +0,0 @@ |
||||
@import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700); |
||||
|
||||
body { |
||||
padding:50px; |
||||
font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; |
||||
color:#777; |
||||
font-weight:300; |
||||
} |
||||
|
||||
h1, h2, h3, h4, h5, h6 { |
||||
color:#222; |
||||
margin:0 0 20px; |
||||
} |
||||
|
||||
p, ul, ol, table, pre, dl { |
||||
margin:0 0 20px; |
||||
} |
||||
|
||||
h1, h2, h3 { |
||||
line-height:1.1; |
||||
} |
||||
|
||||
h1 { |
||||
font-size:28px; |
||||
} |
||||
|
||||
h2 { |
||||
color:#393939; |
||||
} |
||||
|
||||
h3, h4, h5, h6 { |
||||
color:#494949; |
||||
} |
||||
|
||||
a { |
||||
color:#39c; |
||||
font-weight:400; |
||||
text-decoration:none; |
||||
} |
||||
|
||||
a small { |
||||
font-size:11px; |
||||
color:#777; |
||||
margin-top:-0.6em; |
||||
display:block; |
||||
} |
||||
|
||||
.wrapper { |
||||
width:860px; |
||||
margin:0 auto; |
||||
} |
||||
|
||||
blockquote { |
||||
border-left:1px solid #e5e5e5; |
||||
margin:0; |
||||
padding:0 0 0 20px; |
||||
font-style:italic; |
||||
} |
||||
|
||||
code, pre { |
||||
font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; |
||||
color:#333; |
||||
font-size:12px; |
||||
} |
||||
|
||||
pre { |
||||
padding:8px 15px; |
||||
background: #f8f8f8; |
||||
border-radius:5px; |
||||
border:1px solid #e5e5e5; |
||||
overflow-x: auto; |
||||
} |
||||
|
||||
table { |
||||
width:100%; |
||||
border-collapse:collapse; |
||||
} |
||||
|
||||
th, td { |
||||
text-align:left; |
||||
padding:5px 10px; |
||||
border-bottom:1px solid #e5e5e5; |
||||
} |
||||
|
||||
dt { |
||||
color:#444; |
||||
font-weight:700; |
||||
} |
||||
|
||||
th { |
||||
color:#444; |
||||
} |
||||
|
||||
img { |
||||
max-width:100%; |
||||
} |
||||
|
||||
header { |
||||
width:270px; |
||||
float:left; |
||||
position:fixed; |
||||
} |
||||
|
||||
header ul { |
||||
list-style:none; |
||||
height:40px; |
||||
|
||||
padding:0; |
||||
|
||||
background: #eee; |
||||
background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); |
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); |
||||
background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); |
||||
background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); |
||||
background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); |
||||
background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); |
||||
|
||||
border-radius:5px; |
||||
border:1px solid #d2d2d2; |
||||
box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; |
||||
width:270px; |
||||
} |
||||
|
||||
header li { |
||||
width:89px; |
||||
float:left; |
||||
border-right:1px solid #d2d2d2; |
||||
height:40px; |
||||
} |
||||
|
||||
header ul a { |
||||
line-height:1; |
||||
font-size:11px; |
||||
color:#999; |
||||
display:block; |
||||
text-align:center; |
||||
padding-top:6px; |
||||
height:40px; |
||||
} |
||||
|
||||
strong { |
||||
color:#222; |
||||
font-weight:700; |
||||
} |
||||
|
||||
header ul li + li { |
||||
width:88px; |
||||
border-left:1px solid #fff; |
||||
} |
||||
|
||||
header ul li + li + li { |
||||
border-right:none; |
||||
width:89px; |
||||
} |
||||
|
||||
header ul a strong { |
||||
font-size:14px; |
||||
display:block; |
||||
color:#222; |
||||
} |
||||
|
||||
section { |
||||
width:500px; |
||||
float:right; |
||||
padding-bottom:50px; |
||||
} |
||||
|
||||
small { |
||||
font-size:11px; |
||||
} |
||||
|
||||
hr { |
||||
border:0; |
||||
background:#e5e5e5; |
||||
height:1px; |
||||
margin:0 0 20px; |
||||
} |
||||
|
||||
footer { |
||||
width:270px; |
||||
float:left; |
||||
position:fixed; |
||||
bottom:50px; |
||||
} |
||||
|
||||
@media print, screen and (max-width: 960px) { |
||||
|
||||
div.wrapper { |
||||
width:auto; |
||||
margin:0; |
||||
} |
||||
|
||||
header, section, footer { |
||||
float:none; |
||||
position:static; |
||||
width:auto; |
||||
} |
||||
|
||||
header { |
||||
padding-right:320px; |
||||
} |
||||
|
||||
section { |
||||
border:1px solid #e5e5e5; |
||||
border-width:1px 0; |
||||
padding:20px 0; |
||||
margin:0 0 20px; |
||||
} |
||||
|
||||
header a small { |
||||
display:inline; |
||||
} |
||||
|
||||
header ul { |
||||
position:absolute; |
||||
right:50px; |
||||
top:52px; |
||||
} |
||||
} |
||||
|
||||
@media print, screen and (max-width: 720px) { |
||||
body { |
||||
word-wrap:break-word; |
||||
} |
||||
|
||||
header { |
||||
padding:0; |
||||
} |
||||
|
||||
header ul, header p.view { |
||||
position:static; |
||||
} |
||||
|
||||
pre, code { |
||||
word-wrap:normal; |
||||
} |
||||
} |
||||
|
||||
@media print, screen and (max-width: 480px) { |
||||
body { |
||||
padding:15px; |
||||
} |
||||
|
||||
header ul { |
||||
display:none; |
||||
} |
||||
} |
||||
|
||||
@media print { |
||||
body { |
||||
padding:0.4in; |
||||
font-size:12pt; |
||||
color:#444; |
||||
} |
||||
} |
@ -0,0 +1,26 @@ |
||||
/* 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); |
||||
}); |
||||
|
||||
}); |
||||
|
||||
}); |
@ -0,0 +1,34 @@ |
||||
/* 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)); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,35 @@ |
||||
/* 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])); |
||||
} |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,24 @@ |
||||
/* 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')); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,54 @@ |
||||
/* 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