Browse Source

v1.0.0

master
Sven Slootweg 7 years ago
parent
commit
80ddb08e0b
  1. 11
      .gitignore
  2. 263
      app.coffee
  3. 56
      bin/www.coffee
  4. 51
      error-reporter.coffee
  5. 35
      frontend/index.coffee
  6. 199
      frontend/lib/donate.coffee
  7. 33
      frontend/lib/embed.coffee
  8. 16
      frontend/lib/form-popup.coffee
  9. 49
      frontend/lib/scroll-float.coffee
  10. 176
      frontend/lib/upload.coffee
  11. 17
      gen-hash.coffee
  12. 87
      graphics/ia-icon.svg
  13. 95
      graphics/no-thumbnail.svg
  14. 85
      gulpfile.js
  15. 17
      knexfile.coffee
  16. 5
      lib/disable-in-maintenance-mode.coffee
  17. 25
      lib/get-rates.coffee
  18. 7
      lib/middleware-auth.coffee
  19. 11
      lib/parse-amount.coffee
  20. 5
      lib/parse-boolean.coffee
  21. 71
      lib/persist-brute.coffee
  22. 40
      lib/persist-session.coffee
  23. 74
      lib/persist.coffee
  24. 15
      lib/random-string.coffee
  25. 34
      lib/rate-limiter.coffee
  26. 12
      lib/stream-contains.coffee
  27. 13
      lib/tap-error.coffee
  28. 95
      lib/task-runner.coffee
  29. 15
      lib/template-util.coffee
  30. 4
      lib/use-csrf.coffee
  31. 8
      migrations/20150228184510_cdn-storage.coffee
  32. 8
      migrations/20150228185518_thumbnails.coffee
  33. 8
      migrations/20150321122401_public-index.coffee
  34. 13
      migrations/20150321141933_blog.coffee
  35. 61
      migrations/20150321183245_persist-init.coffee
  36. 9
      migrations/20150321220357_abuse.coffee
  37. 24
      migrations/20150321232319_persist-tasks-completed.coffee
  38. 33
      migrations/20150322001330_persist-more.coffee
  39. 9
      migrations/20150418225818_abuse-reason.coffee
  40. 6
      models/blogpost.coffee
  41. 6
      models/document.coffee
  42. 13
      models/index.coffee
  43. 77
      package.json
  44. 11
      public/css/pure-min.css
  45. 714
      public/css/style.css
  46. 1274
      public/css/style.scss
  47. BIN
      public/images/ia.png
  48. BIN
      public/images/logos/bitcoin.png
  49. BIN
      public/images/logos/flattr.png
  50. BIN
      public/images/logos/gratipay.png
  51. BIN
      public/images/logos/paypal.png
  52. BIN
      public/images/logos/sepa.png
  53. BIN
      public/images/no-thumbnail.png
  54. 13418
      public/js/bundle.js
  55. 4
      public/js/jquery-1.11.0.min.js
  56. BIN
      public/pdfjs/cmaps/78-EUC-H.bcmap
  57. BIN
      public/pdfjs/cmaps/78-EUC-V.bcmap
  58. BIN
      public/pdfjs/cmaps/78-H.bcmap
  59. BIN
      public/pdfjs/cmaps/78-RKSJ-H.bcmap
  60. BIN
      public/pdfjs/cmaps/78-RKSJ-V.bcmap
  61. BIN
      public/pdfjs/cmaps/78-V.bcmap
  62. BIN
      public/pdfjs/cmaps/78ms-RKSJ-H.bcmap
  63. BIN
      public/pdfjs/cmaps/78ms-RKSJ-V.bcmap
  64. BIN
      public/pdfjs/cmaps/83pv-RKSJ-H.bcmap
  65. BIN
      public/pdfjs/cmaps/90ms-RKSJ-H.bcmap
  66. BIN
      public/pdfjs/cmaps/90ms-RKSJ-V.bcmap
  67. BIN
      public/pdfjs/cmaps/90msp-RKSJ-H.bcmap
  68. BIN
      public/pdfjs/cmaps/90msp-RKSJ-V.bcmap
  69. BIN
      public/pdfjs/cmaps/90pv-RKSJ-H.bcmap
  70. BIN
      public/pdfjs/cmaps/90pv-RKSJ-V.bcmap
  71. BIN
      public/pdfjs/cmaps/Add-H.bcmap
  72. BIN
      public/pdfjs/cmaps/Add-RKSJ-H.bcmap
  73. BIN
      public/pdfjs/cmaps/Add-RKSJ-V.bcmap
  74. BIN
      public/pdfjs/cmaps/Add-V.bcmap
  75. BIN
      public/pdfjs/cmaps/Adobe-CNS1-0.bcmap
  76. BIN
      public/pdfjs/cmaps/Adobe-CNS1-1.bcmap
  77. BIN
      public/pdfjs/cmaps/Adobe-CNS1-2.bcmap
  78. BIN
      public/pdfjs/cmaps/Adobe-CNS1-3.bcmap
  79. BIN
      public/pdfjs/cmaps/Adobe-CNS1-4.bcmap
  80. BIN
      public/pdfjs/cmaps/Adobe-CNS1-5.bcmap
  81. BIN
      public/pdfjs/cmaps/Adobe-CNS1-6.bcmap
  82. BIN
      public/pdfjs/cmaps/Adobe-CNS1-UCS2.bcmap
  83. BIN
      public/pdfjs/cmaps/Adobe-GB1-0.bcmap
  84. BIN
      public/pdfjs/cmaps/Adobe-GB1-1.bcmap
  85. BIN
      public/pdfjs/cmaps/Adobe-GB1-2.bcmap
  86. BIN
      public/pdfjs/cmaps/Adobe-GB1-3.bcmap
  87. BIN
      public/pdfjs/cmaps/Adobe-GB1-4.bcmap
  88. BIN
      public/pdfjs/cmaps/Adobe-GB1-5.bcmap
  89. BIN
      public/pdfjs/cmaps/Adobe-GB1-UCS2.bcmap
  90. BIN
      public/pdfjs/cmaps/Adobe-Japan1-0.bcmap
  91. BIN
      public/pdfjs/cmaps/Adobe-Japan1-1.bcmap
  92. BIN
      public/pdfjs/cmaps/Adobe-Japan1-2.bcmap
  93. BIN
      public/pdfjs/cmaps/Adobe-Japan1-3.bcmap
  94. BIN
      public/pdfjs/cmaps/Adobe-Japan1-4.bcmap
  95. BIN
      public/pdfjs/cmaps/Adobe-Japan1-5.bcmap
  96. BIN
      public/pdfjs/cmaps/Adobe-Japan1-6.bcmap
  97. BIN
      public/pdfjs/cmaps/Adobe-Japan1-UCS2.bcmap
  98. BIN
      public/pdfjs/cmaps/Adobe-Korea1-0.bcmap
  99. BIN
      public/pdfjs/cmaps/Adobe-Korea1-1.bcmap
  100. BIN
      public/pdfjs/cmaps/Adobe-Korea1-2.bcmap

11
.gitignore

@ -1,3 +1,12 @@
# https://git-scm.com/docs/gitignore
# https://help.github.com/articles/ignoring-files
# Example .gitignore files: https://github.com/github/gitignore
# Example .gitignore files: https://github.com/github/gitignore
/storage/
/node_modules/
/config.json
/persist/
/errors/
/TODO
/thumbnails/
/logs/
.sass-cache

263
app.coffee

@ -0,0 +1,263 @@
express = require('express')
uuid = require "uuid"
fs = require "fs"
domain = require "domain"
app = express()
reportError = (err, type = "error", sync = false) ->
errorPayload = {}
Object.getOwnPropertyNames(err).forEach (key) ->
errorPayload[key] = err[key]
filename = "errors/#{type}-#{Date.now()}-#{uuid.v4()}.json"
if sync
fs.writeFileSync filename, JSON.stringify(errorPayload)
else
fs.writeFile filename, JSON.stringify(errorPayload)
# To make absolutely sure that we can log and exit on all uncaught errors.
if app.get("env") == "production"
logAndExit = (err) ->
reportError(err, "error", true)
process.exit(1)
rootDomain = domain.create()
rootDomain.on "error", logAndExit
runWrapper = (func) ->
rootDomain.run ->
try
func()
catch err
logAndExit(err)
else
# In development, we don't have to catch uncaught errors. Just run the specified function directly.
runWrapper = (func) -> func()
runWrapper ->
path = require('path')
favicon = require('serve-favicon')
logger = require('morgan')
cookieParser = require('cookie-parser')
bodyParser = require('body-parser')
fancyshelf = require "fancyshelf"
session = require "express-session"
csurf = require "csurf"
fileStreamRotator = require "file-stream-rotator"
domainMiddleware = require("express-domain-middleware")
errors = require "errors"
PrettyError = require "pretty-error"
Convert = require "ansi-to-html"
marked = require "marked"
moment = require "moment"
persist = require "./lib/persist"
useCsrf = require "./lib/use-csrf"
templateUtil = require("./lib/template-util")
sessionStore = require("./lib/persist-session")(session)
config = require "./config.json"
knexfile = require("./knexfile").development
ansiHTML = new Convert(escapeXML: true, newline: true)
# Error handling
pe = PrettyError.start()
pe.skipPackage "bluebird", "coffee-script", "express", "express-promise-router", "jade"
pe.skipNodeFiles()
errors.create
name: "UploadError"
errors.create
name: "UploadTooLarge"
parents: errors.UploadError
status: 413
errors.create
name: "InvalidFiletype"
parents: errors.UploadError
status: 415
errors.create
name: "NotAuthenticated"
status: 403
errors.create
name: "InvalidInput"
status: 422
# Database setup
shelf = fancyshelf
engine: knexfile.client
host: knexfile.connection.host
username: knexfile.connection.user
password: knexfile.connection.password
database: knexfile.connection.database
debug: (app.get("env") == "development")
# Task runner
TaskRunner = require("./lib/task-runner")
runner = new TaskRunner(app: app, db: shelf, config: config, thumbnailPath: path.join(__dirname, 'thumbnails'))
runner.addTask "mirror", require("./tasks/mirror"), maxConcurrency: 5
runner.addTask "thumbnail", require("./tasks/thumbnail")
runner.run()
runner.on "taskQueued", (taskType, task) ->
persist.increment "task:#{taskType}:queued"
runner.on "taskStarted", (taskType, task) ->
persist.decrement "task:#{taskType}:queued"
persist.increment "task:#{taskType}:running"
runner.on "taskFailed", (taskType, task) ->
persist.decrement "task:#{taskType}:running"
persist.increment "task:#{taskType}:failed"
runner.on "taskCompleted", (taskType, task) ->
persist.decrement "task:#{taskType}:running"
persist.increment "task:#{taskType}:completed"
if app.get("env") == "development"
runner.on "taskFailed", (taskType, task, err) ->
console.log err.stack
else
runner.on "taskFailed", (taskType, task, err) ->
reportError err, "taskFailed"
# Configure Express
app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'jade')
# Middleware
if app.get("env") == "development"
app.use(logger('dev'))
else
accessLogStream = fileStreamRotator.getStream frequency: (config.accessLog.frequency ? "24h"), filename: config.accessLog.filename
app.use logger (config.accessLog.format ? "combined"), stream: accessLogStream
# Using /static/ paths to maintain backwards compatibility.
app.use("/static/thumbs", express.static(path.join(__dirname, 'thumbnails')))
app.use("/static/pdfjs", express.static(path.join(__dirname, 'public/pdfjs')))
app.use(express.static(path.join(__dirname, 'public')))
#app.use(favicon(__dirname + '/public/favicon.ico'))
app.use domainMiddleware
app.use templateUtil
app.use shelf.express
app.use session
store: new sessionStore
persist: persist
secret: config.session.signingKey
resave: true # TODO: Implement `touch` for the session store, and/or switch to a different store.
saveUninitialized: false
# Load models
require("./models")(shelf)
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(cookieParser())
app.use (req, res, next) ->
res.locals.md = marked
req.appConfig = config
req.persist = persist
jadeExports = [
"announcementVisible"
"announcementText"
"announcementLinkText"
"announcementLink"
"maintenanceMode"
"maintenanceModeText"
"donationGoal"
"donationTotal"
"showNotice"
]
for key in jadeExports
res.locals[key] = persist.getItem "var:#{key}"
res.locals.bitcoinAddress = config.donations.bitcoinAddress
res.locals.isAdmin = req.session.isAdmin
# This is for logging/reporting errors that should not occur, but that are known to not cause any adverse side-effects.
# This should NOT be used for errors that could leave the application in an undefined state!
req.reportError = reportError
req.taskRunner = runner
next()
app.use "/", require("./routes/index")
app.use "/donate", require("./routes/donate")
app.use "/d", require("./routes/document")
app.use "/upload", require("./routes/upload")
app.use "/gallery", require("./routes/gallery")
app.use "/blog", require("./routes/blog")
app.use "/admin", csurf(), useCsrf, require("./routes/admin")
# If no routes match, cause a 404
app.use (req, res, next) ->
next new errors.Http404Error("The requested page was not found.")
# Error handling middleware
app.use "/static/thumbs", (err, req, res, next) ->
# TODO: For some reason, Chrome doesn't always display images that are sent with a 404 status code. Let's stick with 200 for now...
#res.status 404
if err.status == '404'
fs.createReadStream path.join(__dirname, "public/images/no-thumbnail.png")
.pipe res
else
next(err)
app.use (err, req, res, next) ->
statusCode = err.status || 500
res.status(statusCode)
# Dump the error to disk if it's a 500, so that the error reporter can deal with it.
if app.get("env") != "development" and statusCode == 500
reportError(err)
# Display the error to the user - amount of details depending on whether the application is running in production mode or not.
if (app.get('env') == 'development')
stack = err
else
if statusCode == 500
errorMessage = "An unknown error occurred."
stack = {stack: "An administrator has been notified, and the error will be resolved as soon as possible. Apologies for the inconvenience."}
else
errorMessage = err.message
stack = {stack: err.explanation, subMessage: err.subMessage}
if req.headers["X-Requested-With"] == "XMLHttpRequest"
res.send {
message: errorMessage,
error: stack
}
else
if err instanceof errors.NotAuthenticated
res.redirect "/admin/login"
else
if app.get("env") == "development"
htmlStack = ansiHTML.toHtml(stack.stack)
.replace /#555/g, "#b5b5b5"
# TODO: It seems aborted responses will result in an attempt to send an error page/header - of course this can't succeed, as the headers have already been sent by that time, and an error is thrown.
res.render('error', {
message: errorMessage,
error: stack,
statusCode: statusCode,
htmlStack: htmlStack
})
module.exports = app

56
bin/www.coffee

@ -0,0 +1,56 @@
#!/usr/bin/env coffee
app = require('../app')
debug = require('debug')('pdfy:server')
http = require('http')
normalizePort = (val) ->
port = parseInt val, 10
if isNaN port
return val
if port >= 0
return port
return false
onError = (error) ->
if error.syscall != "listen"
throw error
bind = if typeof port == "string"
"Pipe #{port}"
else
"Port #{port}"
switch error.code
when "EACCES"
console.error "#{bind} requires elevated privileges"
process.exit 1
when "EADDRINUSE"
console.error "#{bind} is already in use"
process.exit 1
else
throw error
onListening = ->
addr = server.address()
bind = if typeof port == "string"
"pipe #{port}"
else
"port #{port}"
debug("Listening on #{bind}")
port = normalizePort(process.env.PORT || '3000')
app.set('port', port)
server = http.createServer(app)
server.listen(port)
server.on('error', onError)
server.on('listening', onListening)

51
error-reporter.coffee

@ -0,0 +1,51 @@
chokidar = require "chokidar"
nodemailer = require "nodemailer"
path = require "path"
fs = require "fs"
watcher = chokidar.watch "./errors", depth: 0, ignoreInitial: true
mailer = nodemailer.createTransport()
processFile = (filePath) ->
fs.readFile filePath, (err, data) ->
try
parsedData = JSON.parse(data)
catch error
console.log "Error report not complete yet, retrying #{filePath} in 1 second..."
setTimeout (->
processFile(filePath)
), 1000
return
errorMessage = parsedData?.message ? "UNKNOWN ERROR"
textStack = parsedData?.stack?.replace(/\u001b(?:\[\??(?:\d\;*)*[A-HJKSTfminsulh])?/g, "") ? ""
message = """
A failure occurred. #{filePath} is attached.
#{textStack}
"""
htmlMessage = """
A failure occurred. #{filePath} is attached.
<pre>#{textStack}</pre>
""".replace(/\n/g, "<br>")
mailer.sendMail
from: "ops@pdf.yt"
to: "admin@cryto.net"
subject: "Automatic failure report: #{errorMessage}"
text: message
html: htmlMessage
attachments: [
filename: path.basename(filePath)
path: filePath
contentType: "application/json"
]
watcher.on "add", (filePath) ->
console.log "PANIC! Sending report:", filePath
processFile(filePath)
console.log "Running..."

35
frontend/index.coffee

@ -0,0 +1,35 @@
require "./lib/upload"
require "./lib/embed"
require "./lib/donate"
require "./lib/form-popup"
$ = require "jquery"
autosize = require "autosize"
marked = require "marked"
scrollFloat = require "./lib/scroll-float"
$ ->
$(".checkAll").on "change", (event) ->
newValue = $(this).prop("checked")
$(this)
.closest "table"
.find "input[type='checkbox']"
.filter ->
return !$(this).hasClass "checkAll"
.prop "checked", newValue
scrollFloat($(".floating"))
autosize($(".md-editor"))
updatePreview = ->
$(".md-preview").html(marked($(this).val()))
$(".md-editor")
.on "change input propertychange", updatePreview
.each updatePreview

199
frontend/lib/donate.coffee

@ -0,0 +1,199 @@
$ = require "jquery"
require "Base64"
$ ->
selectedMethod = undefined
selectedAmount = undefined
selectedHasPrice = undefined
pulsateRemove = undefined
paypalHandler = (block) ->
block.find "input.amount"
.val (Math.round(selectedAmount * 100) / 100)
paymentMethodHandlers =
paypal: paypalHandler
"paypal-weekly": paypalHandler
"paypal-monthly": paypalHandler
bitcoin: (block) ->
block.find(".loading-message").show()
block.find(".loaded-content").hide()
$.get "/donate/convert/btc?amount=#{selectedAmount}", (btcAmount) ->
$.get "/donate/bip21?amount=#{btcAmount}", (uri) ->
$(".bip21").attr("href", uri)
block.find(".loading-message").hide()
block.find(".loaded-content").show()
$(".bip21-qr").attr("src", "/donate/bip21/qr?amount=#{btcAmount}")
$(".btc-amount").text(Math.round(btcAmount * 100000000) / 100000000)
setCustomValue = (value) ->
value = parseFloat(value)
if isNaN value
# TODO: Validation error!
value = 0
selectedAmount = value
$ "#custom_amount_input"
.on "keyup", (event) ->
setCustomValue $(this).val()
showInstructions()
.on "change", (event) ->
setCustomValue $(this).val()
showInstructions()
.on "click", (event) ->
#$ this
# .closest ".option"
# .find "input[type='radio']"
# .click()
#event.stopPropagation()
$ "#amount_custom"
.on "click", (event) ->
setCustomValue $("#custom_amount_input").val()
$ ".donation-page section.types .option input[type='radio']"
.on "click", (event) ->
# Hide any instructions that were already visible.
# TODO: Automatically show instructions again when switching back...?
$ ".donation-page section.instructions div"
.hide()
$ ".donation-page section.instructions .placeholder"
.show()
type = $ this
.closest ".option"
.data "type"
$ ".donation-page section.methods .option"
.hide()
$ ".donation-page section.methods .option[data-type='#{type}']"
.show()
$ ".donation-page section.methods .option input[type='radio']"
.on "click", (event) ->
optionElement = $ this
.closest ".option"
selectedMethod = method = optionElement.data "name"
selectedHasPrice = setPrice = !!(optionElement.data "set-price")
if setPrice
$ ".donation-page section.amount"
.slideDown(400)
instructionSection = $ ".donation-page section.instructions"
instructionSection
.children "h3.set-amount"
.show()
instructionSection
.children "h3.no-set-amount"
.hide()
else
$ ".donation-page section.amount"
.slideUp(400)
instructionSection = $ ".donation-page section.instructions"
instructionSection
.children "h3.set-amount"
.hide()
instructionSection
.children "h3.no-set-amount"
.show()
showInstructions()
$ ".donation-page section.amount .option input[type='radio']"
.on "click", (event) ->
if not ($(this).attr("id") == "amount_custom")
selectedAmount = $(this).val()
showInstructions()
showInstructions = ->
$ ".donation-page section.instructions .method"
.hide()
if selectedMethod? and (selectedAmount? or !selectedHasPrice)
$ ".donation-page section.instructions .placeholder"
.hide()
$ ".donation-page section.instructions .method-#{selectedMethod}"
.show()
#$ "html, body"
# .animate scrollTop: "#{$('.donation-page section.instructions').offset().top}px", 500
$ ".donation-page section.instructions"
.addClass "pulsate"
if pulsateRemove?
clearTimeout pulsateRemove
pulsateRemove = setTimeout (->
$ ".donation-page section.instructions"
.removeClass "pulsate"
), 3000
if selectedMethod of paymentMethodHandlers
paymentMethodHandlers[selectedMethod]($(".donation-page section.instructions .method-#{selectedMethod}"))
else
$ ".donation-page section.instructions .placeholder"
.show()
$ ".donation-page .option input[type='radio']"
.on "click", (event) ->
$ this
.closest "section"
.find ".option"
.removeClass "selected"
$ this
.closest ".option"
.addClass "selected"
$ this
.closest ".option"
.find "input[type='number']"
.focus()
event.stopPropagation()
$ ".donation-page .option"
.on "click", (event) ->
$ this
.find "input[type='radio']"
.click()
$ ".donation-page section.types .option[data-type='once']"
.click()
$ ".donation-page #amount_10"
.click()
$ ".donation-page section.instructions .method, .donation-page section.instructions h3.no-set-amount"
.hide()
$ ".js-unavailable"
.hide()
$ ".js-available"
.show()
$ ".decodable"
.each ->
$(this).html atob($(this).html())

33
frontend/lib/embed.coffee

@ -0,0 +1,33 @@
$ = require "jquery"
castBoolean = (value) ->
if value == true
return 1
else
return 0
updateEmbedCode = ->
showToolbar = $("#show_toolbar").prop("checked")
showDonationLink = $("#show_donation").prop("checked")
embedCode = embed_template
.replace "{SPARSE}", castBoolean(not showToolbar)
.replace "{DONATION}", castBoolean(showDonationLink)
$(".embed_code").val embedCode
$ ->
$ ".autoselect"
.on "click", (event) ->
$ this
.focus()
.select()
$ "#show_toolbar, #show_donation"
.on "change", (event) ->
updateEmbedCode()
# Linkify has a tendency of breaking our embed codes, so we re-set the embed code here to make sure that that doesn't happen.
if embed_template?
updateEmbedCode()
# do things and stuff

16
frontend/lib/form-popup.coffee

@ -0,0 +1,16 @@
$ = require "jquery"
$ ->
$(".popup").hide()
$(".form-popup").each ->
elem = $(this)
target = ".popup-#{elem.data('popup')}"
elem.on "click", ->
$(target).show()
$(".popup .close").on "click", ->
$(this)
.closest ".popup"
.hide()

49
frontend/lib/scroll-float.coffee

@ -0,0 +1,49 @@
$ = require "jquery"
module.exports = (jqueryObj) ->
jqueryObj.each ->
elem = $(this)
minTop = threshold = basePos = undefined
currentlyFloating = false
originalPositioning = elem.css "position"
originalLeft = elem.css "left"
originalTop = elem.css "top"
originalLeft = elem.css "right"
updateMetrics = ->
needRestore = currentlyFloating
if needRestore
makeNotFloating()
basePos = elem.offset()
minTop = elem.data("min-top") ? 0
threshold = basePos.top - minTop
if needRestore
makeFloating()
makeFloating = ->
currentlyFloating = true
elem.css
position: "fixed"
top: "#{minTop}px"
right: "auto"
left: "#{basePos.left}px"
makeNotFloating = ->
currentlyFloating = false
elem.attr "style", ""
#elem.css
# position: originalPositioning
# top: originalTop
updateMetrics()
$(window).on "resize", updateMetrics
$(document).on "scroll", (event) ->
if $(document).scrollTop() >= threshold
makeFloating()
else
makeNotFloating()

176
frontend/lib/upload.coffee

@ -0,0 +1,176 @@
$ = require "jquery"
prettyUnits = require "pretty-units"
# The AMD loader for this package doesn't work for some reason - so we explicitly disable it. This will force it to fall back to the CommonJS API.
require "imports?define=>false!blueimp-file-upload"
data_object = null
uploadDone = (response) ->
switch response.status
when 415
errorHeader = "Oops! That's not a PDF file."
errorMessage = "The file you tried to upload is not a valid PDF file. Currently, only PDF files are accepted."
when 413
errorHeader = "Oops! That file is too big."
errorMessage = "The file you tried to upload is too big. Currently, you can only upload PDF files up to 150MB in size."
when 200 # Nothing, success!
else
errorHeader = "Oops! Something went wrong."
errorMessage = "An unknown error occurred. Please reload the page and try again. If the error keeps occurring, <a href='mailto:pdfy@cryto.net'>send us an e-mail</a>!"
if errorMessage?
triggerError errorHeader, errorMessage
reinitializeUploader()
else
if response.responseJSON.redirect?
window.location = response.responseJSON.redirect
else
# TODO: Wat do?
triggerError = (header, message) ->
$(".upload-form .privacySettings, .upload-form .progress, .upload-form .button-submit").hide()
$(".upload").removeClass("faded")
errorBox = $("#uploadError")
.show()
errorBox.find "h3"
.html header
errorBox.find ".message"
.html message
data_object = null
reinitializeUploader = ->
$("#upload_element").replaceWith($("#upload_element").clone(true))
filePicked = (data) ->
$(".upload-form .privacySettings, .upload-form .button-submit").show()
$("#uploadError").hide()
$(".upload-form .fileinfo").removeClass("faded")
fileinfo = $(".fileinfo")
filesize = data.files[0].size
# TODO: Use filesize limit from configuration file!
if filesize > (150 * 1024 * 1024)
reinitializeUploader()
triggerError("Oops! That file is too big.", "The file you tried to upload is too big. Currently, you can only upload PDF files up to 150MB in size.")
return
filesize_text = prettyUnits(filesize) + "B"
fileinfo.find ".filename"
.text data.files[0].name
fileinfo.find ".filesize"
.text filesize_text
$ ".info"
.hide()
fileinfo
.show()
$ ".upload"
.addClass "faded"
updateUploadProgress = (event) ->
if event.lengthComputable
percentage = event.loaded / event.total * 100
done_text = prettyUnits(event.loaded) + "B"
total_text = prettyUnits(event.total) + "B"
progress = $ ".progress"
progress.find ".done"
.text done_text
progress.find ".total"
.text total_text
progress.find ".percentage"
.text (Math.ceil(percentage * 100) / 100)
progress.find ".bar-inner"
.css width: "#{percentage}%"
if event.loaded >= event.total
# Completed!
progress.find ".numbers"
.hide()
progress.find ".wait"
.show()
$ ->
if $().fileupload?
# Only run this if the fileupload plugin is loaded; we don't need all this on eg. the 'view' page.
$ "#upload_form"
.fileupload
fileInput: null
type: "POST"
url: "/upload"
paramName: "file"
autoUpload: false
maxNumberOfFiles: 1
formData: (form) ->
form = $ "#upload_form"
form.serializeArray()
progressall: (e, data) ->
updateUploadProgress
lengthComputable: true
loaded: data.loaded
total: data.total
add: (e, data) ->
data_object = data
filePicked(data)
always: (e, data) ->
uploadDone(data.jqXHR)
$ "#upload_activator"
.on "click", (event) ->
$("#upload_element").click()
$ "#upload_element"
.on "change", (event) ->
filePicked(this)
$ "#upload_form"
.on "submit", (event) ->
event.stopPropagation()
event.preventDefault()
$ ".fileinfo"
.addClass "faded"
$ ".progress"
.show()
if data_object == null
# Only do this if the drag-and-drop dropzone hasn't been used.
formData = new FormData(this)
$.ajax
method: "POST"
url: "/upload"
data: formData
cache: false
contentType: false
processData: false
xhr: ->
customHandler = $.ajaxSettings.xhr()
if customHandler.upload?
customHandler.upload.addEventListener "progress", updateUploadProgress, false
return customHandler
complete: (result) ->
uploadDone(result)
else
# If the dropzone was used...
data_object.submit()

17
gen-hash.coffee

@ -0,0 +1,17 @@
#!/usr/bin/env coffee
scrypt = require "scrypt-for-humans"
Promise = require "bluebird"
read = Promise.promisify(require "read")
Promise.try ->
read(prompt: "Enter a password:", silent: true)
.spread (password, isDefault) ->
if password.trim().length == 0
console.log "You didn't enter a password!"
process.exit(1)
scrypt.hash(password)
.then (hash) ->
console.log "Hash:", hash
console.log "Set this hash in your config.json to use it."

87
graphics/ia-icon.svg

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
width="12.000021"
height="13.574016"
viewBox="0 0 12.000021 13.574015"
enable-background="new 0 0 599.998 583.111"
xml:space="preserve"
id="svg3039"
inkscape:version="0.48.4 r9939"
sodipodi:docname="ia-icon.svg"><metadata
id="metadata3096"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3094" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1125"
id="namedview3092"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="7.9867624"
inkscape:cx="34.68682"
inkscape:cy="12.715741"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="A" /><g
id="A"
transform="translate(-140.28052,-561.46621)"><rect
x="140.28052"
y="574.42487"
width="12.000021"
height="0.61536318"
id="rect3058"
style="fill:#ffffff" /><rect
x="140.75385"
y="573.24133"
width="11.076934"
height="0.8520959"
id="rect3060"
style="fill:#ffffff" /><rect
x="140.68288"
y="563.53723"
width="11.017765"
height="1.1834365"
id="rect3062"
style="fill:#ffffff" /><polygon
points="347.701,162.012 539.506,162.012 551.193,149.072 347.701,101.486 144.21,149.072 155.897,162.012 "
id="polygon3064"
transform="matrix(0.02835123,0,0,0.02835123,136.33399,558.58896)"
style="fill:#ffffff" /><path
d="m 142.27205,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0474,-1.74323 -0.0213,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0222,-0.0574 -0.0615,-0.066 -0.1614,-0.0351 -0.32366,-0.0517 -0.4866,-0.052 -0.16294,2.8e-4 -0.32517,0.0169 -0.48659,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0266,0.54706 -0.0618,1.09375 -0.0832,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.18084,0.047 0.36017,0.0742 0.53937,0.0766 0.17922,-9.3e-4 0.35851,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0222,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0287,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
id="path3084"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
d="m 145.27192,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0474,-1.74323 -0.0214,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0223,-0.0574 -0.0615,-0.066 -0.16139,-0.0351 -0.32366,-0.0517 -0.48656,-0.052 -0.16295,2.8e-4 -0.3252,0.0169 -0.4866,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0265,0.54706 -0.0618,1.09375 -0.0832,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.18089,0.047 0.3602,0.0742 0.53938,0.0766 0.17921,-9.3e-4 0.35849,-0.0294 0.53939,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0222,-0.27872 0.0268,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0286,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
id="path3086"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
d="m 148.75103,568.4671 c -0.009,-0.58121 -0.0245,-1.16243 -0.0473,-1.74323 -0.0214,-0.54724 -0.0566,-1.09393 -0.0832,-1.64099 -9.4e-4,-0.0473 -0.0223,-0.0574 -0.0616,-0.066 -0.16137,-0.0351 -0.32363,-0.0517 -0.48656,-0.052 -0.16293,2.8e-4 -0.32518,0.0169 -0.48661,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0265,0.54706 -0.0618,1.09375 -0.0833,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0505,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0222,0.27872 0.18088,0.047 0.36017,0.0742 0.53938,0.0766 0.17919,-9.3e-4 0.3585,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0221,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0287,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
id="path3088"
inkscape:connector-curvature="0"
style="fill:#ffffff" /><path
d="m 151.67987,568.4671 c -0.009,-0.58121 -0.0246,-1.16243 -0.0474,-1.74323 -0.0214,-0.54724 -0.0567,-1.09393 -0.0833,-1.64099 -9.2e-4,-0.0473 -0.0222,-0.0574 -0.0615,-0.066 -0.16135,-0.0351 -0.32364,-0.0517 -0.48657,-0.052 -0.1629,2.8e-4 -0.32518,0.0169 -0.48655,0.052 -0.0392,0.009 -0.0592,0.0187 -0.0615,0.066 -0.0266,0.54706 -0.0618,1.09375 -0.0833,1.64099 -0.0228,0.5808 -0.0379,1.16202 -0.0474,1.74323 -0.006,0.4119 -0.002,0.82408 0.005,1.23609 0.007,0.45704 0.0174,0.91421 0.0358,1.37092 0.0199,0.49014 0.0506,0.97987 0.0775,1.4697 0.005,0.0939 0.0148,0.18749 0.0221,0.27872 0.1809,0.047 0.3602,0.0742 0.53938,0.0766 0.17921,-9.3e-4 0.35852,-0.0294 0.53938,-0.0766 0.007,-0.0912 0.017,-0.18488 0.0221,-0.27872 0.027,-0.48983 0.0576,-0.97956 0.0775,-1.4697 0.0185,-0.45671 0.0286,-0.91388 0.0358,-1.37092 0.006,-0.41201 0.0113,-0.82419 0.005,-1.23609 z"
id="path3090"
inkscape:connector-curvature="0"
style="fill:#ffffff" /></g></svg>

95
graphics/no-thumbnail.svg

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="180"
height="261"
id="svg4889"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="no-thumbnail.svg">
<defs
id="defs4891" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="88.419794"
inkscape:cy="5.8296482"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1125"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata4894">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-791.36218)">
<g
id="g5420"
transform="translate(4.8995525,0)">
<path
inkscape:connector-curvature="0"
id="path5414"
d="m 32.763905,870.12363 0,98.58436 104.673085,0 0,-141.62912 -56.606425,0 -48.06666,43.04476 z"
style="fill:#f3f2f2;fill-opacity:1;stroke:#e3e3e3;stroke-width:1.14982712;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="rect4901"
d="m 80.830565,827.07887 -48.06666,43.04476 48.06666,0 0,-43.04476 z"
style="fill:#f3f2f2;fill-opacity:1;stroke:#e3e3e3;stroke-width:1.05590618;stroke-linejoin:bevel;stroke-opacity:1" />
<text
sodipodi:linespacing="125%"
id="text5416"
y="949.12549"
x="85.945068"
style="font-size:78.12385559px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#e0e0e0;fill-opacity:1;stroke:none;font-family:Sans"
xml:space="preserve"><tspan
y="949.12549"
x="85.945068"
id="tspan5418"
sodipodi:role="line">?</tspan></text>
</g>
<text
xml:space="preserve"
style="font-size:31.59504318px;font-style:normal;font-weight:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#b3b3b3;fill-opacity:1;stroke:none;font-family:Sans"
x="19.971004"
y="1014.6339"
id="text5426"
sodipodi:linespacing="125%"><tspan
sodipodi:role="line"
id="tspan5428"
x="19.971004"
y="1014.6339"
style="font-size:18.95702553px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#b3b3b3;font-family:Luxi Sans;-inkscape-font-specification:Luxi Sans">no thumbnail yet</tspan></text>
</g>
</svg>

85
gulpfile.js

@ -0,0 +1,85 @@
var gulp = require('gulp');
/* CoffeeScript compile deps */
var path = require('path');
var gutil = require('gulp-util');
var concat = require('gulp-concat');
var rename = require('gulp-rename');
var coffee = require('gulp-coffee');
var cache = require('gulp-cached');
var remember = require('gulp-remember');
var plumber = require('gulp-plumber');
var livereload = require('gulp-livereload');
var nodemon = require("gulp-nodemon");
var net = require("net");
var webpack = require("gulp-webpack");
var sass = require("gulp-sass");
task = {
"source": ["public/**/*.coffee", "routes/**/*.coffee", "models/**/*.coffee", "tasks/**/*.coffee", "app.coffee", "util.coffee", "migrate.coffee", "db.coffee"]
}
/*
gulp.task('coffee', function() {
return gulp.src(task.source, {base: "."})
.pipe(plumber())
.pipe(cache("coffee"))
.pipe(coffee({bare: true}).on('error', gutil.log)).on('data', gutil.log)
.pipe(remember("coffee"))
.pipe(gulp.dest("."));
});*/
gulp.task('webpack', function(){
return gulp.src("frontend/index.coffee")
.pipe(webpack({
watch: true,
module: {
loaders: [{ test: /\.coffee$/, loader: "coffee-loader" }]
},
resolve: { extensions: ["", ".web.coffee", ".web.js", ".coffee", ".js"] }
}))
.pipe(rename("bundle.js"))
.pipe(gulp.dest("public/js/"));
});
gulp.task('sass', function(){
// TODO: Put the source SCSS file in a more logical place...
return gulp.src("./public/css/*.scss")
.pipe(sass())
.pipe(gulp.dest("./public/css"));
});
function checkServerUp(){
setTimeout(function(){
var sock = new net.Socket();
sock.setTimeout(50);
sock.on("connect", function(){
console.log("Trigger page reload...");
livereload.changed();
sock.destroy();
})
.on("timeout", checkServerUp)
.on("error", checkServerUp)
.connect(3000);
}, 70);
}
gulp.task('watch', function () {
livereload.listen();
gulp.watch(['./**/*.css', 'views/**/*.jade', 'package.json', "./public/js/**/*.js"]).on('change', livereload.changed);
gulp.watch(["./public/css/style.scss"], ["sass"])
//gulp.watch(task.source, ['coffee']);
// theseus disabled for now, it was screwing with my tracebacks
//nodemon({script: "./bin/www", ext: "js", nodeArgs: ['/usr/bin/node-theseus']}).on("start", checkServerUp);
nodemon({
script: "./bin/www.coffee",
ext: "coffee",
delay: 500,
ignore: ["./frontend/"],
watch: ["app.coffee", "bin", "lib", "models", "routes", "tasks"]
}).on("start", checkServerUp).on("restart", function(file){
console.log("Restarted triggered by:", file);
});
});
gulp.task('default', [/*'coffee',*/ 'sass', 'watch', 'webpack']);

17
knexfile.coffee

@ -0,0 +1,17 @@
# Update with your config settings.
config = require "./config.json"
module.exports =
# TODO: Do we need an environment name here?
development:
client: "mysql2"
connection:
database: config.database.database
user: config.database.username
password: config.database.password
pool:
min: 2
max: 10
migrations:
tableName: "knex_migrations"

5
lib/disable-in-maintenance-mode.coffee

@ -0,0 +1,5 @@
module.exports = (req, res, next) ->
if (not res.locals.maintenanceMode) or req.session.isAdmin
next()
else
res.status(503).send(res.locals.maintenanceModeText)

25
lib/get-rates.coffee

@ -0,0 +1,25 @@
Promise = require "bluebird"
bhttp = require "bhttp"
lastRates = null
lastRateCheck = 0
module.exports = ->
Promise.try ->
if Date.now() > lastRateCheck + (5 * 60 * 1000)
# We need fresh API data, 5 minutes have elapsed.
Promise.try ->
Promise.all [
bhttp.get "http://api.fixer.io/latest", decodeJSON: true
bhttp.get "https://blockchain.info/ticker", decodeJSON: true
]
.spread (fixerRates, blockchainRates) ->
eurRates = fixerRates.body.rates
eurRates.BTC = 1 / blockchainRates.body.EUR["15m"]
Promise.resolve eurRates
.then (rates) ->
lastRates = rates
lastRateCheck = Date.now()
Promise.resolve rates
else
Promise.resolve lastRates

7
lib/middleware-auth.coffee

@ -0,0 +1,7 @@
errors = require "errors"
module.exports = (req, res, next) ->
if req.session?.isAdmin?
next()
else
next(new errors.NotAuthenticated("You are not logged in as an administrator."))

11
lib/parse-amount.coffee

@ -0,0 +1,11 @@
errors = require "errors"
amountRegex = /^[0-9]+(?:\.[0-9]+)?$/
module.exports = (amount) ->
parsedAmount = parseFloat(amount)
if amountRegex.exec(amount) == null or isNaN(parsedAmount)
throw new errors.InvalidInput("The specified amount is invalid.")
return parsedAmount

5
lib/parse-boolean.coffee

@ -0,0 +1,5 @@
module.exports = (param) ->
if not param?
return undefined
else
return !!(parseInt(param))

71
lib/persist-brute.coffee

@ -0,0 +1,71 @@
# NOTE: This module does not currently ensure correct writes. Callbacks are called immediately (but asynchronously).
AbstractClientStore = require "express-brute/lib/AbstractClientStore"
module.exports = class PersistBruteStore extends AbstractClientStore
constructor: (options) ->
@persist = options.persist
@prefix = options.prefix ? "brute"
@_timers = {}
@_keyMatcher = new RegExp("^#{@prefix}:")
@persist.keys()
.filter (key) => key.match(@_keyMatcher)
.forEach (key) =>
@_createExpiryTimer key, @persist.getItem(key).expiry
_createExpiryTimer: (key, expiry) ->
@_removeExpiryTimer(key)
ttl = expiry - Date.now()
if ttl < 0
@_createRemover(key)()
else
setTimeout @_createRemover(key), ttl
_removeExpiryTimer: (key) ->
if @_timers[key]?
clearTimeout @_timers[key]
delete @_timers[key]
_createRemover: (key) ->
return =>
@_removeExpiryTimer(key)
prefixedKey = "#{@prefix}:#{key}"
# TODO: Error handling?
@persist.removeItem prefixedKey
set: (key, value, lifetime, callback) ->
prefixedKey = "#{@prefix}:#{key}"
expiry = (Date.now() + lifetime)
@_createExpiryTimer key, expiry
@persist.setItem prefixedKey, {value: value, expiry: expiry}, callback
process.nextTick ->
callback(null)
get: (key, callback) ->
prefixedKey = "#{@prefix}:#{key}"
result = @persist.getItem(prefixedKey)
value = result?.value
# Normalize to dates if we're reading from disk-persisted data... on disk, they're saved as strings.
if value?.firstRequest? and value?.firstRequest not instanceof Date
value.firstRequest = new Date(value.firstRequest)
if value?.lastRequest? and value?.lastRequest not instanceof Date
value.lastRequest = new Date(value.lastRequest)
process.nextTick ->
callback(null, value)
reset: (key, callback) ->
# I don't really understand why this is called 'reset'. It's quite clearly a 'remove' function...
@_createRemover(key)()
process.nextTick ->
callback(null)

40
lib/persist-session.coffee

@ -0,0 +1,40 @@
# This will break horribly in a multi-process setup! Don't do that!
# NOTE: Does not currently ensure writes.
module.exports = (session) ->
class PersistSessionStore extends session.Store
constructor: (options) ->
@persist = options.persist
get: (sid, callback) ->
sessionData = @persist.getItem "session:#{sid}"
process.nextTick ->
callback(null, sessionData)
set: (sid, sessionData, callback) ->
sessionData.__lastAccess = Date.now()
@persist.setItem "session:#{sid}", sessionData
process.nextTick ->
callback(null)
destroy: (sid, session, callback) ->
@persist.removeItem "session:#{sid}"
process.nextTick ->
callback(null)
length: (callback) ->
length = @persist.valuesWithKeyMatch(/^session:/).length
process.nextTick ->
callback(null, length)
clear: (callback) ->
@persist.keys()
.filter (key) -> key.match(/^session:/)
.forEach (key) ->
@persist.removeItem key
process.nextTick ->
callback(null)
# TODO: .touch

74
lib/persist.coffee

@ -0,0 +1,74 @@
Promise = require "bluebird"
persist = require "node-persist"
path = require "path"
xtend = require "xtend"
# We MUST explicitly specify the `persist` directory, otherwise node-persist will bug out and write to its own module directory...
persist.initSync(continuous: false, dir: path.join(__dirname, "../persist"))
persist.increment = (key, amount = 1) ->
persist.setItem key, (persist.getItem(key) + amount)
persist.decrement = (key, amount = 1) ->
persist.setItem key, (persist.getItem(key) - amount)
persist.addListItem = (key, item) ->
newList = [item].concat (persist.getItem(key) ? [])
persist.setItem key, newList
persist.removeListItem = (key, item) ->
newList = (persist.getItem(key) ? [])
.filter (existingItem) ->
return (item == existingItem)
persist.setItem key, newList
persist.removeListItemByFilter = (key, filter) ->
newList = (persist.getItem(key) ? [])
.filter (item) ->
return !filter(item)
persist.setItem key, newList
persist.setProperty = (key, propertyKey, value) ->
newObj = {}
newObj[propertyKey] = value
oldObj = persist.get(key)
persist.setItem key, xtend(oldObj, newObj)
persist.removeProperty = (key, propertyKey) ->
# Extremely ghetto shallow clone
obj = xtend({}, persist.get(key))
delete obj[propertyKey]
persist.setItem key, obj
# Rough shim for write queueing...
writeQueue = []
currentlyWriting = false
_setItem = persist.setItem
persist.setItem = (key, value) ->
new Promise (resolve, reject) ->
_setItem.call(persist, key, value)
addItemToQueue key, value, resolve, reject
triggerWrite()
addItemToQueue = (key, value, resolveFunc, rejectFunc) ->
writeQueue.push [key, value, resolveFunc, rejectFunc]
triggerWrite = ->
if not currentlyWriting and writeQueue.length > 0
currentlyWriting = 1
[key, value, resolveFunc, rejectFunc] = writeQueue.shift()
Promise.resolve(persist.persistKey(key))
.then (result) -> resolveFunc(result)
.catch (err) -> rejectFunc(err)
.finally ->
currentlyWriting = false
triggerWrite()
module.exports = persist

15
lib/random-string.coffee

@ -0,0 +1,15 @@
Promise = require "bluebird"
crypto = Promise.promisifyAll(require "crypto")
module.exports = (length = 16) ->
Promise.try ->
byteLength = Math.ceil(length / 4) * 3
return crypto.randomBytesAsync(byteLength)
.then (bytes) ->
bytes = bytes
.toString "base64"
.replace /\+/g, "-"
.replace /\//g, "_"
.slice 0, length
Promise.resolve bytes

34
lib/rate-limiter.coffee

@ -0,0 +1,34 @@
class RateLimiter
constructor: (@limit, @interval, @funcA, @funcB) ->
@_totalCalls = 0
@_startTimer()
_startTimer: ->
@_timer = setInterval @_clearCalls, @interval
_stopTimer: ->
if @_timer?
clearInterval @_timer
@_timer = null
_clearCalls: ->
@_totalCalls = 0
call: ->
@_totalCalls += 1
targetFunc = switch
when @_totalCalls <= @limit then @funcA
else @funcB
targetFunc.apply this, arguments
setInterval: (interval) ->
@_stopTimer()
@interval = interval
@_startTimer()
setLimit: (limit) ->
@limit = limit
module.exports = ->
return (funcA, funcB, options) ->
if not options.limit?
throw new Error("No limit specified.")
options.interval ?= 60 # Default: 60 seconds ie. 1 minute.
return new RateLimiter(options.limit, options.interval, funcA, funcB)

12
lib/stream-contains.coffee

@ -0,0 +1,12 @@
Promise = require "bluebird"
concatStream = require "concat-stream"
buffertools = require "buffertools"
module.exports = (stream, needle) ->
# CAUTION: This buffers up in memory!
new Promise (resolve, reject) ->
stream
.pipe concatStream (result) ->
resolve buffertools.indexOf(result, needle) != -1
.on "error", (err) ->
reject err

13
lib/tap-error.coffee

@ -0,0 +1,13 @@
# NOTE: This is purely a `tap` equivalent for errors! Any resolves or rejections are ignored - promises can only be used to wait for async execution of something.
Promise = require "bluebird"
module.exports = (func) ->
return (err) ->
Promise.try ->
func(err)
.catch ->
# Consume any errors
Promise.resolve()
.then ->
Promise.reject(err)

95
lib/task-runner.coffee

@ -0,0 +1,95 @@
EventEmitter = require("events").EventEmitter
Promise = require "bluebird"
debug = require("debug")("task-runner")
makeExternalPromise = ->
extResolve = extReject = null
new Promise (resolve, reject) ->
extResolve = resolve
extReject = reject
module.exports = class TaskRunner extends EventEmitter
constructor: (@context = {}) ->
@_taskTypes = {}
@_queue = {}
@_runningCount = {}
@running = false
@context.taskRunner = this
_checkRunTask: ->
if not @running
return
debug "checking for runnable tasks..."
for taskType, options of @_taskTypes