DEBUG = false
MAX_CONCURRENT = 25
{EventEmitter} = require('events')
async = require('async')
_ = require('underscore')
syncstring = require('./syncstring')
synctable = require('./synctable')
db_doc = require('./db-doc')
smc_version = require('./smc-version')
message = require("./message")
misc = require("./misc")
{validate_client_query} = require('./schema-validate')
defaults = misc.defaults
required = defaults.required
JSON_CHANNEL = '\u0000'
exports.JSON_CHANNEL = JSON_CHANNEL
DEFAULT_TIMEOUT = 30
class Session extends EventEmitter
constructor: (opts) ->
opts = defaults opts,
conn : required
project_id : required
session_uuid : required
params : undefined
data_channel : undefined
init_history : undefined
@start_time = misc.walltime()
@conn = opts.conn
@params = opts.params
if @type() == 'console'
if not @params?.path? or not @params?.filename?
throw Error("params must be specified with path and filename")
@project_id = opts.project_id
@session_uuid = opts.session_uuid
@data_channel = opts.data_channel
@init_history = opts.init_history
@emit("open")
if @reconnect?
@conn.on("connected", @reconnect)
close: () =>
@removeAllListeners()
if @reconnect?
@conn.removeListener("connected", @reconnect)
reconnect: (cb) =>
if not @conn._signed_in
setTimeout((()=>@reconnect(cb)), 500)
return
if @_reconnect_lock
cb?("reconnect: hit lock")
return
@emit "reconnecting"
@_reconnect_lock = true
f = (cb) =>
@conn.call
message : message.connect_to_session
session_uuid : @session_uuid
type : @type()
project_id : @project_id
params : @params
timeout : 30
cb : (err, reply) =>
if err
cb(err); return
switch reply.event
when 'error'
cb(reply.error)
when 'session_connected'
if @data_channel != reply.data_channel
@conn.change_data_channel
prev_channel : @data_channel
new_channel : reply.data_channel
session : @
@data_channel = reply.data_channel
@init_history = reply.history
cb()
else
cb("bug in hub")
misc.retry_until_success
max_time : 15000
factor : 1.3
f : f
cb : (err) =>
delete @_reconnect_lock
if not err
@emit("reconnect")
cb?(err)
terminate_session: (cb) =>
@conn.call
message :
message.terminate_session
project_id : @project_id
session_uuid : @session_uuid
timeout : 30
cb : cb
walltime: () =>
return misc.walltime() - @start_time
handle_data: (data) =>
@emit("data", data)
write_data: (data) ->
@conn.write_data(@data_channel, data)
restart: (cb) =>
@conn.call(message:message.restart_session(session_uuid:@session_uuid), timeout:10, cb:cb)
class ConsoleSession extends Session
type: () => "console"
class exports.Connection extends EventEmitter
constructor: (@url) ->
@setMaxListeners(3000)
@emit("connecting")
@_call =
queue : []
count : 0
@_id_counter = 0
@_sessions = {}
@_new_sessions = {}
@_data_handlers = {}
@execute_callbacks = {}
@call_callbacks = {}
@_project_title_cache = {}
@_usernames_cache = {}
@_redux = undefined
@register_data_handler(JSON_CHANNEL, @handle_json_data)
@on 'connected', @send_version
@_connect @url, (data) =>
if data.length > 0
channel = data[0]
data = data.slice(1)
@_handle_data(channel, data)
@emit("data", channel, data)
@_connected = false
@_ping_interval = 60000
@_ping()
dbg: (f) =>
return (m...) ->
switch m.length
when 0
s = ''
when 1
s = m[0]
else
s = JSON.stringify(m)
console.log("#{(new Date()).toISOString()} - Client.#{f}: #{s}")
_ping: () =>
@_ping_interval ?= 60000
@_last_ping = new Date()
@call
message : message.ping()
timeout : 15
cb : (err, pong) =>
if not err
now = new Date()
if pong?.event == 'pong' and now - @_last_ping <= 1000*15
@_last_pong = {server:pong.now, local:now}
@_clock_skew = @_last_ping - 0 + ((@_last_pong.local - @_last_ping)/2) - @_last_pong.server
misc.set_local_storage('clock_skew', @_clock_skew)
setTimeout(@_ping, @_ping_interval)
server_time: =>
t = @_server_time()
last = @_last_server_time
if last? and last >= t
t = new Date((last - 0) + 1)
@_last_server_time = t
return t
_server_time: =>
if not @_clock_skew?
x = misc.get_local_storage('clock_skew')
if x?
@_clock_skew = parseFloat(x)
if @_clock_skew?
return new Date(new Date() - @_clock_skew)
else
return new Date()
ping_test: (opts) =>
opts = defaults opts,
packets : 20
timeout : 5
delay_ms : 200
log : undefined
cb : undefined
ping_times = []
do_ping = (i, cb) =>
t = new Date()
@call
message : message.ping()
timeout : opts.timeout
cb : (err, pong) =>
heading = "#{i}/#{opts.packets}: "
if not err and pong?.event == 'pong'
ping_time = new Date() - t
bar = ('*' for j in [0...Math.floor(ping_time/10)]).join('')
mesg = "#{heading}time=#{ping_time}ms"
else
bar = "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
mesg = "#{heading}Request error -- #{err}, #{misc.to_json(pong)}"
ping_time = Infinity
while mesg.length < 40
mesg += ' '
mesg += bar
if opts.log?
opts.log(mesg)
else
console.log(mesg)
ping_times.push(ping_time)
setTimeout(cb, opts.delay_ms)
async.mapSeries([1..opts.packets], do_ping, (err) => opts.cb?(err, ping_times))
close: () =>
@_conn.close()
version: =>
return smc_version.version
send_version: =>
@send(message.version(version:@version()))
send: (mesg) =>
@write_data(JSON_CHANNEL, misc.to_json_socket(mesg))
write_data: (channel, data) =>
try
@_write(channel + data)
catch err
is_signed_in: =>
return @is_connected() and !!@_signed_in
client_id: () =>
return @account_id
is_project: () =>
return false
is_user: () =>
return true
is_connected: => !!@_connected
remember_me_key: => "remember_me#{window?.app_base_url ? ''}"
handle_json_data: (data) =>
mesg = misc.from_json_socket(data)
if DEBUG
console.log("handle_json_data: #{data}")
switch mesg.event
when "execute_javascript"
if mesg.session_uuid?
@_sessions[mesg.session_uuid].emit("execute_javascript", mesg)
else
@emit("execute_javascript", mesg)
when "output"
cb = @execute_callbacks[mesg.id]
if cb?
cb(mesg)
delete @execute_callbacks[mesg.id] if mesg.done
if mesg.session_uuid?
@_sessions[mesg.session_uuid].emit("output", mesg)
else
@emit("output", mesg)
when "terminate_session"
session = @_sessions[mesg.session_uuid]
session?.emit("close")
when "session_reconnect"
if mesg.data_channel?
@_sessions[mesg.data_channel]?.reconnect()
else if mesg.session_uuid?
@_sessions[mesg.session_uuid]?.reconnect()
when "cookies"
@_cookies?(mesg)
when "signed_in"
@account_id = mesg.account_id
@_signed_in = true
misc.set_local_storage(@remember_me_key(), true)
@_sign_in_mesg = mesg
@emit("signed_in", mesg)
when "remember_me_failed"
misc.delete_local_storage(@remember_me_key())
@emit(mesg.event, mesg)
when "project_list_updated", 'project_data_changed'
@emit(mesg.event, mesg)
when 'version'
@emit('new_version', {version:mesg.version, min_version:mesg.min_version})
when "error"
if not mesg.id?
console.log("WARNING: #{misc.to_json(mesg.error)}")
return
id = mesg.id
v = @call_callbacks[id]
if v?
{cb, error_event} = v
v.first = false
if error_event and mesg.event == 'error'
cb(mesg.error)
else
cb(undefined, mesg)
if not mesg.multi_response
delete @call_callbacks[id]
@emit('message', mesg)
change_data_channel: (opts) =>
opts = defaults opts,
prev_channel : required
new_channel : required
session : required
@unregister_data_handler(opts.prev_channel)
delete @_sessions[opts.prev_channel]
@_sessions[opts.new_channel] = opts.session
@register_data_handler(opts.new_channel, opts.session.handle_data)
register_data_handler: (channel, h) ->
@_data_handlers[channel] = h
unregister_data_handler: (channel) ->
delete @_data_handlers[channel]
_handle_data: (channel, data) =>
f = @_data_handlers[channel]
if f?
f(data)
connect_to_session: (opts) ->
opts = defaults opts,
type : required
session_uuid : required
project_id : required
timeout : DEFAULT_TIMEOUT
params : required
cb : required
@call
message : message.connect_to_session
session_uuid : opts.session_uuid
type : opts.type
project_id : opts.project_id
params : opts.params
timeout : opts.timeout
cb : (error, reply) =>
if error
opts.cb(error); return
switch reply.event
when 'error'
opts.cb(reply.error)
when 'session_connected'
@_create_session_object
type : opts.type
project_id : opts.project_id
session_uuid : opts.session_uuid
data_channel : reply.data_channel
init_history : reply.history
params : opts.params
cb : opts.cb
else
opts.cb("Unknown event (='#{reply.event}') in response to connect_to_session message.")
new_session: (opts) ->
opts = defaults opts,
timeout : DEFAULT_TIMEOUT
type : "console"
params : required
project_id : required
cb : required
@call
message : message.start_session
type : opts.type
params : opts.params
project_id : opts.project_id
timeout : opts.timeout
cb : (error, reply) =>
if error
opts.cb(error)
else
if reply.event == 'error'
opts.cb(reply.error)
else if reply.event == "session_started" or reply.event == "session_connected"
@_create_session_object
type : opts.type
project_id : opts.project_id
session_uuid : reply.session_uuid
data_channel : reply.data_channel
params : opts.params
cb : opts.cb
else
opts.cb("Unknown event (='#{reply.event}') in response to start_session message.")
_create_session_object: (opts) =>
opts = defaults opts,
type : required
project_id : required
session_uuid : required
data_channel : undefined
params : undefined
init_history : undefined
cb : required
session_opts =
conn : @
project_id : opts.project_id
session_uuid : opts.session_uuid
data_channel : opts.data_channel
init_history : opts.init_history
params : opts.params
switch opts.type
when 'console'
session = new ConsoleSession(session_opts)
else
opts.cb("Unknown session type: '#{opts.type}'")
@_sessions[opts.session_uuid] = session
if opts.data_channel != JSON_CHANNEL
@_sessions[opts.data_channel] = session
@register_data_handler(opts.data_channel, session.handle_data)
opts.cb(false, session)
_do_call: (opts, cb) =>
if not opts.cb?
@send(opts.message)
setTimeout(cb, 150)
return
id = opts.message.id ?= misc.uuid()
@call_callbacks[id] =
cb : (args...) =>
if cb?
cb()
cb = undefined
opts.cb(args...)
error_event : opts.error_event
first : true
@send(opts.message)
if opts.timeout
setTimeout(
(() =>
if @call_callbacks[id]?.first
error = "Timeout after #{opts.timeout} seconds"
if cb?
cb()
cb = undefined
opts.cb(error, message.error(id:id, error:error))
delete @call_callbacks[id]
), opts.timeout*1000
)
call: (opts={}) =>
opts = defaults opts,
message : required
timeout : undefined
error_event : false
cb : undefined
if not @is_connected()
opts.cb?('not connected')
return
@_call.queue.push(opts)
@_update_calls()
_update_calls: =>
while @_call.queue.length > 0 and @_call.count <= MAX_CONCURRENT
@_process_next_call()
_process_next_call: =>
if @_call.queue.length == 0
return
@_call.count += 1
@_do_call @_call.queue.shift(), =>
@_call.count -= 1
@_update_calls()
call_local_hub: (opts) =>
opts = defaults opts,
project_id : required
message : required
timeout : undefined
cb : undefined
m = message.local_hub
multi_response : false
project_id : opts.project_id
message : opts.message
timeout : opts.timeout
if opts.cb?
f = (err, resp) =>
opts.cb?(err, resp)
else
f = undefined
@call
message : m
timeout : opts.timeout
cb : f
create_account: (opts) =>
opts = defaults opts,
first_name : required
last_name : required
email_address : required
password : required
agreed_to_terms: required
token : undefined
timeout : 40
cb : required
if not opts.agreed_to_terms
opts.cb(undefined, message.account_creation_failed(reason:{"agreed_to_terms":"Agree to the CoCalc Terms of Service."}))
return
if @_create_account_lock
opts.cb(undefined, message.account_creation_failed(reason:{"account_creation_failed":"You are submitting too many requests to create an account; please wait a second."}))
return
@_create_account_lock = true
@call
message : message.create_account
first_name : opts.first_name
last_name : opts.last_name
email_address : opts.email_address
password : opts.password
agreed_to_terms : opts.agreed_to_terms
token : opts.token
timeout : opts.timeout
cb : (err, resp) =>
setTimeout((() => delete @_create_account_lock), 1500)
opts.cb(err, resp)
delete_account: (opts) =>
opts = defaults opts,
account_id : required
timeout : 40
cb : required
@call
message : message.delete_account
account_id : opts.account_id
timeout : opts.timeout
cb : opts.cb
sign_in_using_auth_token: (opts) ->
opts = defaults opts,
auth_token : required
cb : required
@call
message : message.sign_in_using_auth_token
auth_token : opts.auth_token
timeout : opts.timeout
cb : opts.cb
sign_in: (opts) ->
opts = defaults opts,
email_address : required
password : required
remember_me : false
cb : required
timeout : 40
@call
message : message.sign_in
email_address : opts.email_address
password : opts.password
remember_me : opts.remember_me
timeout : opts.timeout
cb : opts.cb
sign_out: (opts) ->
opts = defaults opts,
everywhere : false
cb : undefined
timeout : DEFAULT_TIMEOUT
@account_id = undefined
@call
message : message.sign_out(everywhere:opts.everywhere)
timeout : opts.timeout
cb : opts.cb
@emit('signed_out')
change_password: (opts) ->
opts = defaults opts,
email_address : required
old_password : ""
new_password : required
cb : undefined
@call
message : message.change_password
email_address : opts.email_address
old_password : opts.old_password
new_password : opts.new_password
cb : opts.cb
change_email: (opts) ->
opts = defaults opts,
new_email_address : required
password : ""
cb : undefined
if not @account_id?
opts.cb?("must be logged in")
return
@call
message: message.change_email_address
account_id : @account_id
new_email_address : opts.new_email_address
password : opts.password
error_event : true
cb : opts.cb
forgot_password: (opts) ->
opts = defaults opts,
email_address : required
cb : required
@call
message: message.forgot_password
email_address : opts.email_address
cb: opts.cb
reset_forgot_password: (opts) ->
opts = defaults(opts,
reset_code : required
new_password : required
cb : required
timeout : DEFAULT_TIMEOUT
)
@call(
message : message.reset_forgot_password(reset_code:opts.reset_code, new_password:opts.new_password)
cb : opts.cb
)
unlink_passport: (opts) ->
opts = defaults opts,
strategy : required
id : required
cb : undefined
@call
message : message.unlink_passport
strategy : opts.strategy
id : opts.id
error_event : true
timeout : 15
cb : opts.cb
api_key: (opts) ->
opts = defaults opts,
action : required
password : required
cb : required
if not @account_id?
opts.cb?("must be logged in")
return
@call
message: message.api_key
action : opts.action
password : opts.password
error_event : true
timeout : 10
cb : (err, resp) ->
opts.cb(err, resp?.api_key)
create_project: (opts) =>
opts = defaults opts,
title : required
description : required
cb : undefined
@call
message: message.create_project(title:opts.title, description:opts.description)
cb : (err, resp) =>
if err
opts.cb?(err)
else if resp.event == 'error'
opts.cb?(resp.error)
else
opts.cb?(undefined, resp.project_id)
open_project: (opts) ->
opts = defaults opts,
project_id : required
cb : required
@call
message :
message.open_project
project_id : opts.project_id
cb : opts.cb
write_text_file_to_project: (opts) ->
opts = defaults opts,
project_id : required
path : required
content : required
timeout : DEFAULT_TIMEOUT
cb : undefined
@call
message :
message.write_text_file_to_project
project_id : opts.project_id
path : opts.path
content : opts.content
timeout : opts.timeout
cb : (err, resp) => opts.cb?(err, resp)
read_text_file_from_project: (opts) ->
opts = defaults opts,
project_id : required
path : required
cb : required
timeout : DEFAULT_TIMEOUT
@call
message :
message.read_text_file_from_project
project_id : opts.project_id
path : opts.path
timeout : opts.timeout
cb : opts.cb
read_file_from_project: (opts) ->
opts = defaults opts,
project_id : required
path : required
timeout : DEFAULT_TIMEOUT
archive : 'tar.bz2'
cb : undefined
base = window?.app_base_url ? ''
if opts.path[0] == '/'
opts.path = '.smc/root' + opts.path
url = misc.encode_path("#{base}/#{opts.project_id}/raw/#{opts.path}")
opts.cb?(false, {url:url})
return url
project_branch_op: (opts) ->
opts = defaults opts,
project_id : required
branch : required
op : required
cb : required
@call
message : message["#{opts.op}_project_branch"]
project_id : opts.project_id
branch : opts.branch
cb : opts.cb
stopped_editing_file: (opts) =>
opts = defaults opts,
project_id : required
filename : required
cb : undefined
@call
message : message.stopped_editing_file
project_id : opts.project_id
filename : opts.filename
cb : opts.cb
invite_noncloud_collaborators: (opts) =>
opts = defaults opts,
project_id : required
title : required
link2proj : required
replyto : undefined
replyto_name : undefined
to : required
email : required
subject : undefined
cb : required
@call
message: message.invite_noncloud_collaborators
project_id : opts.project_id
title : opts.title
link2proj : opts.link2proj
email : opts.email
replyto : opts.replyto
replyto_name : opts.replyto_name
to : opts.to
subject : opts.subject
cb : (err, resp) =>
if err
opts.cb(err)
else if resp.event == 'error'
if not resp.error
resp.error = "error inviting collaborators"
opts.cb(resp.error)
else
opts.cb(undefined, resp)
copy_path_between_projects: (opts) =>
opts = defaults opts,
public : false
src_project_id : required
src_path : required
target_project_id : required
target_path : undefined
overwrite_newer : false
delete_missing : false
backup : false
timeout : undefined
exclude_history : false
cb : undefined
is_public = opts.public
delete opts.public
cb = opts.cb
delete opts.cb
if not opts.target_path?
opts.target_path = opts.src_path
if is_public
mesg = message.copy_public_path_between_projects(opts)
else
mesg = message.copy_path_between_projects(opts)
@call
message : mesg
cb : (err, resp) =>
if err
cb?(err)
else if resp.event == 'error'
cb?(resp.error)
else
cb?(undefined, resp)
project_set_quotas: (opts) =>
opts = defaults opts,
project_id : required
memory : undefined
cpu_shares : undefined
cores : undefined
disk_quota : undefined
mintime : undefined
network : undefined
member_host : undefined
cb : undefined
cb = opts.cb
delete opts.cb
@call
message : message.project_set_quotas(opts)
cb : (err, resp) =>
if err
cb?(err)
else if resp.event == 'error'
cb?(resp.error)
else
cb?(undefined, resp)
remove_blob_ttls: (opts) =>
opts = defaults opts,
uuids : required
cb : undefined
if opts.uuids.length == 0
opts.cb?()
else
@call
message :
message.remove_blob_ttls
uuids : opts.uuids
cb : (err, resp) =>
if err
opts.cb?(err)
else if resp.event == 'error'
opts.cb?(resp.error)
else
opts.cb?()
public_get_text_file: (opts) =>
opts = defaults opts,
project_id : required
path : required
cb : required
timeout : DEFAULT_TIMEOUT
@call
message :
message.public_get_text_file
project_id : opts.project_id
path : opts.path
timeout : opts.timeout
cb : (err, resp) =>
if err
opts.cb(err)
else if resp.event == 'error'
opts.cb(resp.error)
else
opts.cb(undefined, resp.data)
public_project_directory_listing: (opts) =>
opts = defaults opts,
project_id : required
path : '.'
time : false
start : 0
limit : -1
timeout : DEFAULT_TIMEOUT
hidden : false
cb : required
@call
message :
message.public_get_directory_listing
project_id : opts.project_id
path : opts.path
time : opts.time
start : opts.tart
limit : opts.limit
hidden : opts.hidden
timeout : opts.timeout
cb : (err, resp) =>
if err
opts.cb(err)
else if resp.event == 'error'
opts.cb(resp.error)
else
opts.cb(undefined, resp.result)
exec: (opts) ->
opts = defaults opts,
project_id : required
path : ''
command : required
args : []
timeout : 30
network_timeout : undefined
max_output : undefined
bash : false
err_on_exit : true
cb : required
if not opts.network_timeout?
opts.network_timeout = opts.timeout * 1.5
@call
message : message.project_exec
project_id : opts.project_id
path : opts.path
command : opts.command
args : opts.args
timeout : opts.timeout
max_output : opts.max_output
bash : opts.bash
err_on_exit : opts.err_on_exit
timeout : opts.network_timeout
cb : (err, mesg) ->
if err
opts.cb(err, mesg)
else if mesg.event == 'error'
opts.cb(mesg.error)
else
opts.cb(false, {stdout:mesg.stdout, stderr:mesg.stderr, exit_code:mesg.exit_code})
makedirs: (opts) =>
opts = defaults opts,
project_id : required
path : required
cb : undefined
@exec
project_id : opts.project_id
command : 'mkdir'
args : ['-p', opts.path]
cb : opts.cb
find_directories: (opts) =>
opts = defaults opts,
project_id : required
query : '*'
path : '.'
exclusions : undefined
include_hidden : false
cb : required
args = [opts.path, '-xdev', '!', '-readable', '-prune', '-o', '-type', 'd', '-iname', "'#{opts.query}'", '-readable']
tail_args = ['-print']
if opts.exclusions?
exclusion_args = _.map opts.exclusions, (excluded_path, index) =>
"-a -not \\( -path '#{opts.path}/#{excluded_path}' -prune \\)"
args = args.concat(exclusion_args)
args = args.concat(tail_args)
command = "find #{args.join(' ')}"
@exec
project_id : opts.project_id
command : command
timeout : 15
cb : (err, result) =>
if err
opts.cb?(err); return
if result.event == 'error'
opts.cb?(result.error); return
n = opts.path.length + 1
v = result.stdout.split('\n')
if not opts.include_hidden
v = (x for x in v when x.indexOf('/.') == -1)
v = (x.slice(n) for x in v when x.length > n)
ans =
query : opts.query
path : opts.path
project_id : opts.project_id
directories : v
opts.cb?(undefined, ans)
user_search: (opts) =>
opts = defaults opts,
query : required
query_id : -1
limit : 20
timeout : DEFAULT_TIMEOUT
cb : required
@call
message : message.user_search(query:opts.query, limit:opts.limit)
timeout : opts.timeout
cb : (err, resp) =>
if err
opts.cb(err)
else
opts.cb(undefined, resp.results, opts.query_id)
project_invite_collaborator: (opts) =>
opts = defaults opts,
project_id : required
account_id : required
cb : (err) =>
@call
message : message.invite_collaborator(project_id:opts.project_id, account_id:opts.account_id)
cb : (err, result) =>
if err
opts.cb(err)
else if result.event == 'error'
opts.cb(result.error)
else
opts.cb(undefined, result)
project_remove_collaborator: (opts) =>
opts = defaults opts,
project_id : required
account_id : required
cb : (err) =>
@call
message : message.remove_collaborator(project_id:opts.project_id, account_id:opts.account_id)
cb : (err, result) =>
if err
opts.cb(err)
else if result.event == 'error'
opts.cb(result.error)
else
opts.cb(undefined, result)
get_usernames: (opts) ->
opts = defaults opts,
account_ids : required
use_cache : true
cb : required
usernames = {}
for account_id in opts.account_ids
usernames[account_id] = false
if opts.use_cache
for account_id, done of usernames
if not done and @_usernames_cache[account_id]?
usernames[account_id] = @_usernames_cache[account_id]
account_ids = (account_id for account_id, done of usernames when not done)
if account_ids.length == 0
opts.cb(undefined, usernames)
else
@call
message : message.get_usernames(account_ids : account_ids)
cb : (err, resp) =>
if err
opts.cb(err)
else if resp.event == 'error'
opts.cb(resp.error)
else
for account_id, username of resp.usernames
usernames[account_id] = username
@_usernames_cache[account_id] = username
opts.cb(undefined, usernames)
project_directory_listing: (opts) =>
opts = defaults opts,
project_id : required
path : '.'
timeout : 60
hidden : false
cb : required
base = window?.app_base_url ? ''
if opts.path[0] == '/'
opts.path = '.smc/root' + opts.path
url = misc.encode_path("#{base}/#{opts.project_id}/raw/.smc/directory_listing/#{opts.path}")
url += "?random=#{Math.random()}"
if opts.hidden
url += '&hidden=true'
req = $.ajax
dataType : "json"
url : url
timeout : 3000
success : (data) ->
opts.cb(undefined, data)
req.fail (err) ->
opts.cb(err)
project_get_state: (opts) =>
opts = defaults opts,
project_id : required
cb : required
@call
message:
message.project_get_state
project_id : opts.project_id
cb : (err, resp) ->
if err
opts.cb(err)
else if resp.event == 'error'
opts.cb(resp.error)
else
opts.cb(false, resp.state)
print_to_pdf: (opts) =>
opts = defaults opts,
project_id : required
path : required
timeout : 90
options : undefined
cb : undefined
opts.options.timeout = opts.timeout
@call_local_hub
project_id : opts.project_id
message : message.print_to_pdf
path : opts.path
options : opts.options
timeout : opts.timeout
cb : (err, resp) =>
if err
opts.cb?(err)
else if resp.event == 'error'
if resp.error?
opts.cb?(resp.error)
else
opts.cb?('error')
else
opts.cb?(undefined, resp.path)
log_error: (error) =>
@call(message : message.log_client_error(error:error))
webapp_error: (opts) =>
@call(message : message.webapp_error(opts))
_stripe_call: (mesg, cb) =>
@call
message : mesg
error_event : true
timeout : 15
cb : cb
stripe_get_customer: (opts) =>
opts = defaults opts,
cb : required
@_stripe_call message.stripe_get_customer(), (err, mesg) =>
if err
opts.cb(err)
else
resp =
stripe_publishable_key : mesg.stripe_publishable_key
customer : mesg.customer
opts.cb(undefined, resp)
stripe_create_source: (opts) =>
opts = defaults opts,
token : required
cb : required
@_stripe_call(message.stripe_create_source(token: opts.token), opts.cb)
stripe_delete_source: (opts) =>
opts = defaults opts,
card_id : required
cb : required
@_stripe_call(message.stripe_delete_source(card_id: opts.card_id), opts.cb)
stripe_update_source: (opts) =>
opts = defaults opts,
card_id : required
info : required
cb : required
@_stripe_call(message.stripe_update_source(card_id: opts.card_id, info:opts.info), opts.cb)
stripe_set_default_source: (opts) =>
opts = defaults opts,
card_id : required
cb : required
@_stripe_call(message.stripe_set_default_source(card_id: opts.card_id), opts.cb)
stripe_get_charges: (opts) =>
opts = defaults opts,
limit : undefined
ending_before : undefined
starting_after : undefined
cb : required
@call
message :
message.stripe_get_charges
limit : opts.limit
ending_before : opts.ending_before
starting_after : opts.starting_after
error_event : true
cb : (err, mesg) =>
if err
opts.cb(err)
else
opts.cb(undefined, mesg.charges)
stripe_get_plans: (opts) =>
opts = defaults opts,
cb : required
@call
message : message.stripe_get_plans()
error_event : true
cb : (err, mesg) =>
if err
opts.cb(err)
else
opts.cb(undefined, mesg.plans)
stripe_create_subscription: (opts) =>
opts = defaults opts,
plan : required
quantity : 1
coupon : undefined
cb : required
@call
message : message.stripe_create_subscription
plan : opts.plan
quantity : opts.quantity
coupon : opts.coupon
error_event : true
cb : opts.cb
stripe_cancel_subscription: (opts) =>
opts = defaults opts,
subscription_id : required
at_period_end : true
cb : required
@call
message : message.stripe_cancel_subscription
subscription_id : opts.subscription_id
at_period_end : opts.at_period_end
error_event : true
cb : opts.cb
stripe_update_subscription: (opts) =>
opts = defaults opts,
subscription_id : required
quantity : undefined
coupon : undefined
projects : undefined
plan : undefined
cb : required
@call
message : message.stripe_update_subscription
subscription_id : opts.subscription_id
quantity : opts.quantity
coupon : opts.coupon
projects : opts.projects
plan : opts.plan
error_event : true
cb : opts.cb
stripe_get_subscriptions: (opts) =>
opts = defaults opts,
limit : undefined
ending_before : undefined
starting_after : undefined
cb : required
@call
message :
message.stripe_get_subscriptions
limit : opts.limit
ending_before : opts.ending_before
starting_after : opts.starting_after
error_event : true
cb : (err, mesg) =>
if err
opts.cb(err)
else
opts.cb(undefined, mesg.subscriptions)
stripe_get_invoices: (opts) =>
opts = defaults opts,
limit : 10
ending_before : undefined
starting_after : undefined
cb : required
@call
message :
message.stripe_get_invoices
limit : opts.limit
ending_before : opts.ending_before
starting_after : opts.starting_after
error_event : true
cb : (err, mesg) =>
if err
opts.cb(err)
else
opts.cb(undefined, mesg.invoices)
stripe_admin_create_invoice_item: (opts) =>
opts = defaults opts,
account_id : undefined
email_address : undefined
amount : undefined
description : undefined
cb : required
@call
message : message.stripe_admin_create_invoice_item
account_id : opts.account_id
email_address : opts.email_address
amount : opts.amount
description : opts.description
error_event : true
cb : opts.cb
stripe_admin_create_customer: (opts) =>
opts = defaults opts,
account_id : undefined
email_address : undefined
cb : required
@stripe_admin_create_invoice_item(opts)
create_support_ticket: ({opts, cb}) =>
@call
message : message.create_support_ticket(opts)
timeout : 20
error_event : true
cb : (err, resp) ->
if err
cb?(err)
else
cb?(undefined, resp.url)
get_support_tickets: (cb) =>
@call
message : message.get_support_tickets()
timeout : 20
error_event : true
cb : (err, tickets) ->
if err
cb?(err)
else
cb?(undefined, tickets.tickets)
projects: (opts) =>
opts = defaults opts,
cb : required
@query
query :
projects : [{project_id:null, title:null, description:null, last_edited:null, users:null}]
changes : true
cb : opts.cb
changefeed: (opts) =>
keys = misc.keys(opts)
if keys.length != 1
throw Error("must specify exactly one table")
table = keys[0]
x = {}
if not misc.is_array(opts[table])
x[table] = [opts[table]]
else
x[table] = opts[table]
return @query(query:x, changes: true)
sync_table: (query, options, debounce_interval=2000, throttle_changes=undefined) =>
return synctable.sync_table(query, options, @, debounce_interval, throttle_changes)
sync_string: (opts) =>
opts = defaults opts,
id : undefined
project_id : required
path : required
file_use_interval : 'default'
cursors : false
patch_interval : 1000
opts.client = @
return new syncstring.SyncString(opts)
sync_db: (opts) =>
opts = defaults opts,
project_id : required
path : required
primary_keys : required
string_cols : undefined
cursors : false
change_throttle : 500
save_interval : 2000
patch_interval : 1000
opts.client = @
return new db_doc.SyncDB(opts)
open_existing_sync_document: (opts) =>
opts = defaults opts,
project_id : required
path : required
cb : required
opts.client = @
db_doc.open_existing_sync_document(opts)
return
mark_file: (opts) =>
opts = defaults opts,
project_id : required
path : required
action : required
ttl : 120
@_redux?.getActions('file_use').mark_file(opts.project_id, opts.path, opts.action, opts.ttl)
query: (opts) =>
opts = defaults opts,
query : required
changes : undefined
options : undefined
timeout : 30
cb : undefined
if opts.options? and not misc.is_array(opts.options)
throw Error("options must be an array")
err = validate_client_query(opts.query, @account_id)
if err
opts.cb?(err)
return
mesg = message.query
query : opts.query
options : opts.options
changes : opts.changes
multi_response : opts.changes
@call
message : mesg
error_event : true
timeout : opts.timeout
cb : opts.cb
query_cancel: (opts) =>
opts = defaults opts,
id : required
cb : undefined
@call
message : message.query_cancel(id:opts.id)
error_event : true
timeout : 30
cb : opts.cb
query_get_changefeed_ids: (opts) =>
opts = defaults opts,
cb : required
@call
message : message.query_get_changefeed_ids()
error_event : true
timeout : 30
cb : (err, resp) =>
if err
opts.cb(err)
else
@_changefeed_ids = resp.changefeed_ids
opts.cb(undefined, resp.changefeed_ids)
exports.is_valid_password = (password) ->
if typeof(password) != 'string'
return [false, 'Password must be specified.']
if password.length >= 6 and password.length <= 64
return [true, '']
else
return [false, 'Password must be between 6 and 64 characters in length.']
exports.issues_with_create_account = (mesg) ->
issues = {}
if not mesg.agreed_to_terms
issues.agreed_to_terms = 'Agree to the Salvus Terms of Service.'
if mesg.first_name == ''
issues.first_name = 'Enter your first name.'
if mesg.last_name == ''
issues.last_name = 'Enter your last name.'
if not misc.is_valid_email_address(mesg.email_address)
issues.email_address = 'Email address does not appear to be valid.'
[valid, reason] = exports.is_valid_password(mesg.password)
if not valid
issues.password = reason
return issues