quartz_flow 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/etc/quartz.rb CHANGED
@@ -7,10 +7,10 @@ set :bind, "0.0.0.0"
7
7
  set :port, 4444
8
8
 
9
9
  # Directory where downloaded torrent data will be stored
10
- set :basedir, "/mnt/twotb/movies"
10
+ set :basedir, "download"
11
11
 
12
12
  # Directory where .torrent files and .info files will be stored.
13
- set :metadir, "meta"
13
+ set :metadir, "meta"
14
14
 
15
15
  # TCP port used for torrents
16
16
  set :torrent_port, 9997
@@ -23,3 +23,10 @@ set :torrent_log, "log/torrent.log"
23
23
 
24
24
  # On which day of the month should monthly usage tracking reset
25
25
  set :monthly_usage_reset_day, 5
26
+
27
+ # Torrent Queueing settings.
28
+ # Max number of active torrents is the max number of torrents that can be running at once.
29
+ # Max number of incomplete torrents is a subset of the max active torrents, and describes
30
+ # the max number of torrents that can be running that are not uploading.
31
+ set :torrent_queue_max_active, 10
32
+ set :torrent_queue_max_incomplete, 5
@@ -46,6 +46,8 @@ class Server < Sinatra::Base
46
46
  set :password_file, "etc/passwd"
47
47
  set :logging, true
48
48
  set :monthly_usage_reset_day, 1
49
+ set :torrent_queue_max_incomplete, 5
50
+ set :torrent_queue_max_active, 10
49
51
 
50
52
  # Load configuration settings
51
53
  eval File.open("./etc/quartz.rb","r").read
@@ -72,9 +74,13 @@ class Server < Sinatra::Base
72
74
  setDefaultLevel :info
73
75
  end
74
76
  LogConfigurator.configLevels
75
- peerClient = QuartzTorrent::PeerClient.new(settings.basedir)
77
+ peerClient = QuartzTorrent::PeerClient.new(settings.basedir, settings.torrent_queue_max_incomplete, settings.torrent_queue_max_active)
76
78
  peerClient.port = settings.torrent_port
77
- peerClient.start
79
+ begin
80
+ peerClient.start
81
+ rescue Errno::EADDRINUSE
82
+ raise "Starting torrent peer failed because listening on port #{settings.torrent_port} failed: " + $!.message
83
+ end
78
84
 
79
85
  # Initialize Datamapper
80
86
  #DataMapper::Logger.new($stdout, :debug)
@@ -211,13 +217,15 @@ class Server < Sinatra::Base
211
217
  begin
212
218
  $manager.storeMagnet(magnet)
213
219
  rescue
220
+ puts "Storing magnet link failed: #{$!}"
221
+ puts $!.backtrace.join("\n")
214
222
  halt 500, "Storing magnet link failed: #{$!}."
215
223
  end
216
224
 
217
225
  begin
218
226
  $manager.startMagnet(magnet)
219
227
  rescue
220
- puts $!
228
+ puts "Starting magnet link failed: #{$!}"
221
229
  puts $!.backtrace.join("\n")
222
230
  halt 500, "Starting magnet link failed: #{$!}."
223
231
  end
@@ -254,7 +262,7 @@ class Server < Sinatra::Base
254
262
 
255
263
  infoHash = json["infohash"]
256
264
  halt 500, "Pausing torrent failed: no infohash parameter was sent to the server in the post request." if !infoHash || infoHash.length == 0
257
- $manager.peerClient.setPaused QuartzTorrent::hexToBytes(infoHash), true
265
+ $manager.setTorrentPaused infoHash, true
258
266
  puts "Pausing torrent"
259
267
  "OK"
260
268
  end
@@ -264,7 +272,7 @@ class Server < Sinatra::Base
264
272
 
265
273
  infoHash = json["infohash"]
266
274
  halt 500, "Unpausing torrent failed: no infohash parameter was sent to the server in the post request." if !infoHash || infoHash.length == 0
267
- $manager.peerClient.setPaused QuartzTorrent::hexToBytes(infoHash), false
275
+ $manager.setTorrentPaused infoHash, false
268
276
  "OK"
269
277
  end
270
278
 
@@ -103,6 +103,24 @@ class SettingsHelper
103
103
  @@saveFilterForDuration,
104
104
  Proc.new{ |v| QuartzTorrent::Formatter.formatTime(v) }
105
105
  ),
106
+ :paused => SettingMetainfo.new(
107
+ :paused,
108
+ :torrent,
109
+ Proc.new{ |v| v.to_s },
110
+ Proc.new{ |v| v.downcase == "true" }
111
+ ),
112
+ :bytesUploaded => SettingMetainfo.new(
113
+ :bytesUploaded,
114
+ :torrent,
115
+ Proc.new{ |v| v.to_s },
116
+ Proc.new{ |v| v.to_i }
117
+ ),
118
+ :bytesDownloaded => SettingMetainfo.new(
119
+ :bytesDownloaded,
120
+ :torrent,
121
+ Proc.new{ |v| v.to_s},
122
+ Proc.new{ |v| v.to_i }
123
+ ),
106
124
  }
107
125
 
108
126
  def set(settingName, value, owner = nil)
@@ -8,6 +8,7 @@ class TorrentManager
8
8
  def initialize(peerClient, torrentFileDir, monthlyResetDay)
9
9
  @peerClient = peerClient
10
10
  @cachedTorrentData = nil
11
+ @cachedTorrentDataMutex = Mutex.new
11
12
  @cachedAt = nil
12
13
  @cacheLifetime = 2
13
14
  @torrentFileDir = torrentFileDir
@@ -15,22 +16,28 @@ class TorrentManager
15
16
  @usageTracker = UsageTracker.new(monthlyResetDay)
16
17
  # Start a thread to keep track of usage.
17
18
  startUsageTrackerThread
19
+ startTorrentDataThread
18
20
  end
19
21
 
20
22
  attr_reader :peerClient
21
23
 
22
24
  def torrentData(infoHash = nil)
23
- if (! @cachedTorrentData || Time.new - @cachedAt > @cacheLifetime) && ! @peerClientStopped
24
- @cachedTorrentData = @peerClient.torrentData
25
- @cachedAt = Time.new
25
+ result = nil
26
+
27
+ # The first time, we may need to wait for the thread to load the data.
28
+ sleep(0.25) while ! @cachedTorrentData
29
+
30
+ @cachedTorrentDataMutex.synchronize do
31
+ result = @cachedTorrentData
26
32
  end
27
-
28
- @cachedTorrentData
33
+ result
29
34
  end
30
35
 
31
36
  def stopPeerClient
32
37
  @peerClient.stop
33
38
  @peerClientStopped = true
39
+ stopTorrentDataThread
40
+ stopUsageTrackerThread
34
41
  end
35
42
 
36
43
  # Start torrents that already exist in the torrent file directory
@@ -102,11 +109,15 @@ class TorrentManager
102
109
  h[:uploadRateLimit] = QuartzTorrent::Formatter.formatSpeed(h[:uploadRateLimit])
103
110
  h[:downloadRateLimit] = QuartzTorrent::Formatter.formatSize(h[:downloadRateLimit])
104
111
  h[:bytesUploaded] = QuartzTorrent::Formatter.formatSize(h[:bytesUploaded])
112
+ h[:bytesDownloaded] = QuartzTorrent::Formatter.formatSize(h[:bytesDownloaded])
105
113
  h[:uploadDuration] = QuartzTorrent::Formatter.formatTime(h[:uploadDuration]) if h[:uploadDuration]
106
114
 
107
115
  h[:completePieces] = d.completePieceBitfield ? d.completePieceBitfield.countSet : 0
108
116
  h[:totalPieces] = d.completePieceBitfield ? d.completePieceBitfield.length : 0
109
117
 
118
+ # Only send 8 alarms
119
+ h[:alarms] = h[:alarms].first(8)
120
+
110
121
  if where
111
122
  matches = true
112
123
  where.each do |k,v|
@@ -222,6 +233,15 @@ class TorrentManager
222
233
  FileUtils.rm magnetFile if File.exists?(magnetFile)
223
234
  end
224
235
 
236
+ # Pause or unpause the specified torrent. Store the pause state in the database.
237
+ def setTorrentPaused(infoHash, val)
238
+ infoHashBytes = QuartzTorrent::hexToBytes(infoHash)
239
+ @peerClient.setPaused infoHashBytes, val
240
+
241
+ helper = SettingsHelper.new
242
+ helper.set :paused, val, infoHash
243
+ end
244
+
225
245
  # Update the torrent settings (upload rate limit, etc) from database values
226
246
  def applyTorrentSettings(infoHash)
227
247
  asciiInfoHash = QuartzTorrent::bytesToHex(infoHash)
@@ -241,10 +261,18 @@ class TorrentManager
241
261
  uploadDuration = helper.get(:defaultUploadDuration, :unfiltered) if ! uploadDuration
242
262
  uploadDuration = uploadDuration.to_i if uploadDuration
243
263
 
264
+ paused = helper.get(:paused, :filter, asciiInfoHash)
265
+
266
+ bytesDownloaded = helper.get(:bytesDownloaded, :filter, asciiInfoHash)
267
+ bytesUploaded = helper.get(:bytesUploaded, :filter, asciiInfoHash)
268
+
244
269
  @peerClient.setUploadRateLimit infoHash, uploadRateLimit
245
270
  @peerClient.setDownloadRateLimit infoHash, downloadRateLimit
246
271
  @peerClient.setUploadRatio infoHash, ratio
247
272
  @peerClient.setUploadDuration infoHash, uploadDuration
273
+ @peerClient.setPaused infoHash, paused
274
+ @peerClient.adjustBytesDownloaded infoHash, bytesDownloaded if bytesDownloaded
275
+ @peerClient.adjustBytesUploaded infoHash, bytesUploaded if bytesUploaded
248
276
  end
249
277
 
250
278
  # Get the usage for the current period of the specified type.
@@ -274,6 +302,7 @@ class TorrentManager
274
302
  QuartzTorrent.initThread("torrent_usage_tracking")
275
303
 
276
304
  Thread.current[:stopped] = false
305
+ helper = SettingsHelper.new
277
306
 
278
307
  while ! Thread.current[:stopped]
279
308
  begin
@@ -282,6 +311,9 @@ class TorrentManager
282
311
  usage = 0
283
312
  torrentData.each do |k,v|
284
313
  usage += v.bytesUploaded + v.bytesDownloaded
314
+ asciiInfoHash = QuartzTorrent::bytesToHex(k)
315
+ helper.set :bytesDownloaded, v.bytesDownloaded, asciiInfoHash
316
+ helper.set :bytesUploaded, v.bytesUploaded, asciiInfoHash
285
317
  end
286
318
  @usageTracker.update(usage)
287
319
  rescue
@@ -291,4 +323,42 @@ class TorrentManager
291
323
  end
292
324
  end
293
325
  end
326
+
327
+ def stopUsageTrackerThread
328
+ @usageTrackerThread[:stopped] = true
329
+ end
330
+
331
+ # Start a thread to update torrent data
332
+ def startTorrentDataThread
333
+ @torrentDataThread = Thread.new do
334
+ QuartzTorrent.initThread("torrent_data_loader")
335
+
336
+ Thread.current[:stopped] = false
337
+
338
+ while ! Thread.current[:stopped]
339
+ begin
340
+ timer = Time.new
341
+ if (! @cachedTorrentData || Time.new - @cachedAt > @cacheLifetime) && ! @peerClientStopped
342
+ data = @peerClient.torrentData
343
+ @cachedTorrentDataMutex.synchronize do
344
+ @cachedTorrentData = data
345
+ @cachedAt = Time.new
346
+ end
347
+ end
348
+ timer = Time.new - timer
349
+
350
+ if timer < @cacheLifetime
351
+ sleep @cacheLifetime-timer
352
+ end
353
+ rescue
354
+ puts "Error in torrent data loader thread: #{$!}"
355
+ puts $!.backtrace.join "\n"
356
+ end
357
+ end
358
+ end
359
+ end
360
+
361
+ def stopTorrentDataThread
362
+ @torrentDataThread[:stopped] = true
363
+ end
294
364
  end
@@ -191,7 +191,9 @@ class MonthlyBucketChangeCriteria < BucketChangeCriteria
191
191
  def criteriaData
192
192
  now = Time.new
193
193
  nextMonth = now.mon % 12 + 1
194
- Time.local(now.year, nextMonth, @resetDay)
194
+ year = now.year
195
+ year += 1 if nextMonth == 1
196
+ Time.local(year, nextMonth, @resetDay)
195
197
  end
196
198
  end
197
199
 
@@ -51,6 +51,15 @@ module QuartzTorrent
51
51
  end
52
52
  end
53
53
 
54
+ class Alarm
55
+ def to_h
56
+ result = {}
57
+ result[:details] = @details
58
+ result[:time] = @time
59
+ result
60
+ end
61
+ end
62
+
54
63
  class TorrentDataDelegate
55
64
  # Convert to a hash. Also flattens some of the data into new fields.
56
65
  def to_h
@@ -70,6 +79,8 @@ module QuartzTorrent
70
79
  0
71
80
  end
72
81
  end
82
+ # Cap estimated time at 9999 hours
83
+ secondsLeft = 35996400 if secondsLeft > 35996400
73
84
  result[:timeLeft] = Formatter.formatTime(secondsLeft)
74
85
 
75
86
  ## Regular fields
@@ -87,12 +98,14 @@ module QuartzTorrent
87
98
  result[:metainfoLength] = @metainfoLength
88
99
  result[:metainfoCompletedLength] = @metainfoCompletedLength
89
100
  result[:paused] = @paused
101
+ result[:queued] = @queued
90
102
  result[:uploadRateLimit] = @uploadRateLimit
91
103
  result[:downloadRateLimit] = @downloadRateLimit
92
104
  result[:ratio] = @ratio
93
105
  result[:uploadDuration] = @uploadDuration
94
106
  result[:bytesUploaded] = @bytesUploaded
95
107
  result[:bytesDownloaded] = @bytesDownloaded
108
+ result[:alarms] = @alarms.collect{ |a| a.to_h }
96
109
 
97
110
  result
98
111
  end
@@ -0,0 +1,457 @@
1
+ /**
2
+ * @license AngularJS v1.0.8
3
+ * (c) 2010-2012 Google, Inc. http://angularjs.org
4
+ * License: MIT
5
+ */
6
+ (function(window, angular, undefined) {
7
+ 'use strict';
8
+
9
+ /**
10
+ * @ngdoc overview
11
+ * @name ngResource
12
+ * @description
13
+ */
14
+
15
+ /**
16
+ * @ngdoc object
17
+ * @name ngResource.$resource
18
+ * @requires $http
19
+ *
20
+ * @description
21
+ * A factory which creates a resource object that lets you interact with
22
+ * [RESTful](http://en.wikipedia.org/wiki/Representational_State_Transfer) server-side data sources.
23
+ *
24
+ * The returned resource object has action methods which provide high-level behaviors without
25
+ * the need to interact with the low level {@link ng.$http $http} service.
26
+ *
27
+ * # Installation
28
+ * To use $resource make sure you have included the `angular-resource.js` that comes in Angular
29
+ * package. You can also find this file on Google CDN, bower as well as at
30
+ * {@link http://code.angularjs.org/ code.angularjs.org}.
31
+ *
32
+ * Finally load the module in your application:
33
+ *
34
+ * angular.module('app', ['ngResource']);
35
+ *
36
+ * and you are ready to get started!
37
+ *
38
+ * @param {string} url A parameterized URL template with parameters prefixed by `:` as in
39
+ * `/user/:username`. If you are using a URL with a port number (e.g.
40
+ * `http://example.com:8080/api`), you'll need to escape the colon character before the port
41
+ * number, like this: `$resource('http://example.com\\:8080/api')`.
42
+ *
43
+ * @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
44
+ * `actions` methods.
45
+ *
46
+ * Each key value in the parameter object is first bound to url template if present and then any
47
+ * excess keys are appended to the url search query after the `?`.
48
+ *
49
+ * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
50
+ * URL `/path/greet?salutation=Hello`.
51
+ *
52
+ * If the parameter value is prefixed with `@` then the value of that parameter is extracted from
53
+ * the data object (useful for non-GET operations).
54
+ *
55
+ * @param {Object.<Object>=} actions Hash with declaration of custom action that should extend the
56
+ * default set of resource actions. The declaration should be created in the following format:
57
+ *
58
+ * {action1: {method:?, params:?, isArray:?},
59
+ * action2: {method:?, params:?, isArray:?},
60
+ * ...}
61
+ *
62
+ * Where:
63
+ *
64
+ * - `action` – {string} – The name of action. This name becomes the name of the method on your
65
+ * resource object.
66
+ * - `method` – {string} – HTTP request method. Valid methods are: `GET`, `POST`, `PUT`, `DELETE`,
67
+ * and `JSONP`
68
+ * - `params` – {object=} – Optional set of pre-bound parameters for this action.
69
+ * - isArray – {boolean=} – If true then the returned object for this action is an array, see
70
+ * `returns` section.
71
+ *
72
+ * @returns {Object} A resource "class" object with methods for the default set of resource actions
73
+ * optionally extended with custom `actions`. The default set contains these actions:
74
+ *
75
+ * { 'get': {method:'GET'},
76
+ * 'save': {method:'POST'},
77
+ * 'query': {method:'GET', isArray:true},
78
+ * 'remove': {method:'DELETE'},
79
+ * 'delete': {method:'DELETE'} };
80
+ *
81
+ * Calling these methods invoke an {@link ng.$http} with the specified http method,
82
+ * destination and parameters. When the data is returned from the server then the object is an
83
+ * instance of the resource class. The actions `save`, `remove` and `delete` are available on it
84
+ * as methods with the `$` prefix. This allows you to easily perform CRUD operations (create,
85
+ * read, update, delete) on server-side data like this:
86
+ * <pre>
87
+ var User = $resource('/user/:userId', {userId:'@id'});
88
+ var user = User.get({userId:123}, function() {
89
+ user.abc = true;
90
+ user.$save();
91
+ });
92
+ </pre>
93
+ *
94
+ * It is important to realize that invoking a $resource object method immediately returns an
95
+ * empty reference (object or array depending on `isArray`). Once the data is returned from the
96
+ * server the existing reference is populated with the actual data. This is a useful trick since
97
+ * usually the resource is assigned to a model which is then rendered by the view. Having an empty
98
+ * object results in no rendering, once the data arrives from the server then the object is
99
+ * populated with the data and the view automatically re-renders itself showing the new data. This
100
+ * means that in most case one never has to write a callback function for the action methods.
101
+ *
102
+ * The action methods on the class object or instance object can be invoked with the following
103
+ * parameters:
104
+ *
105
+ * - HTTP GET "class" actions: `Resource.action([parameters], [success], [error])`
106
+ * - non-GET "class" actions: `Resource.action([parameters], postData, [success], [error])`
107
+ * - non-GET instance actions: `instance.$action([parameters], [success], [error])`
108
+ *
109
+ *
110
+ * @example
111
+ *
112
+ * # Credit card resource
113
+ *
114
+ * <pre>
115
+ // Define CreditCard class
116
+ var CreditCard = $resource('/user/:userId/card/:cardId',
117
+ {userId:123, cardId:'@id'}, {
118
+ charge: {method:'POST', params:{charge:true}}
119
+ });
120
+
121
+ // We can retrieve a collection from the server
122
+ var cards = CreditCard.query(function() {
123
+ // GET: /user/123/card
124
+ // server returns: [ {id:456, number:'1234', name:'Smith'} ];
125
+
126
+ var card = cards[0];
127
+ // each item is an instance of CreditCard
128
+ expect(card instanceof CreditCard).toEqual(true);
129
+ card.name = "J. Smith";
130
+ // non GET methods are mapped onto the instances
131
+ card.$save();
132
+ // POST: /user/123/card/456 {id:456, number:'1234', name:'J. Smith'}
133
+ // server returns: {id:456, number:'1234', name: 'J. Smith'};
134
+
135
+ // our custom method is mapped as well.
136
+ card.$charge({amount:9.99});
137
+ // POST: /user/123/card/456?amount=9.99&charge=true {id:456, number:'1234', name:'J. Smith'}
138
+ });
139
+
140
+ // we can create an instance as well
141
+ var newCard = new CreditCard({number:'0123'});
142
+ newCard.name = "Mike Smith";
143
+ newCard.$save();
144
+ // POST: /user/123/card {number:'0123', name:'Mike Smith'}
145
+ // server returns: {id:789, number:'01234', name: 'Mike Smith'};
146
+ expect(newCard.id).toEqual(789);
147
+ * </pre>
148
+ *
149
+ * The object returned from this function execution is a resource "class" which has "static" method
150
+ * for each action in the definition.
151
+ *
152
+ * Calling these methods invoke `$http` on the `url` template with the given `method` and `params`.
153
+ * When the data is returned from the server then the object is an instance of the resource type and
154
+ * all of the non-GET methods are available with `$` prefix. This allows you to easily support CRUD
155
+ * operations (create, read, update, delete) on server-side data.
156
+
157
+ <pre>
158
+ var User = $resource('/user/:userId', {userId:'@id'});
159
+ var user = User.get({userId:123}, function() {
160
+ user.abc = true;
161
+ user.$save();
162
+ });
163
+ </pre>
164
+ *
165
+ * It's worth noting that the success callback for `get`, `query` and other method gets passed
166
+ * in the response that came from the server as well as $http header getter function, so one
167
+ * could rewrite the above example and get access to http headers as:
168
+ *
169
+ <pre>
170
+ var User = $resource('/user/:userId', {userId:'@id'});
171
+ User.get({userId:123}, function(u, getResponseHeaders){
172
+ u.abc = true;
173
+ u.$save(function(u, putResponseHeaders) {
174
+ //u => saved user object
175
+ //putResponseHeaders => $http header getter
176
+ });
177
+ });
178
+ </pre>
179
+
180
+ * # Buzz client
181
+
182
+ Let's look at what a buzz client created with the `$resource` service looks like:
183
+ <doc:example>
184
+ <doc:source jsfiddle="false">
185
+ <script>
186
+ function BuzzController($resource) {
187
+ this.userId = 'googlebuzz';
188
+ this.Activity = $resource(
189
+ 'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments',
190
+ {alt:'json', callback:'JSON_CALLBACK'},
191
+ {get:{method:'JSONP', params:{visibility:'@self'}}, replies: {method:'JSONP', params:{visibility:'@self', comments:'@comments'}}}
192
+ );
193
+ }
194
+
195
+ BuzzController.prototype = {
196
+ fetch: function() {
197
+ this.activities = this.Activity.get({userId:this.userId});
198
+ },
199
+ expandReplies: function(activity) {
200
+ activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id});
201
+ }
202
+ };
203
+ BuzzController.$inject = ['$resource'];
204
+ </script>
205
+
206
+ <div ng-controller="BuzzController">
207
+ <input ng-model="userId"/>
208
+ <button ng-click="fetch()">fetch</button>
209
+ <hr/>
210
+ <div ng-repeat="item in activities.data.items">
211
+ <h1 style="font-size: 15px;">
212
+ <img src="{{item.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
213
+ <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a>
214
+ <a href ng-click="expandReplies(item)" style="float: right;">Expand replies: {{item.links.replies[0].count}}</a>
215
+ </h1>
216
+ {{item.object.content | html}}
217
+ <div ng-repeat="reply in item.replies.data.items" style="margin-left: 20px;">
218
+ <img src="{{reply.actor.thumbnailUrl}}" style="max-height:30px;max-width:30px;"/>
219
+ <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: {{reply.content | html}}
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </doc:source>
224
+ <doc:scenario>
225
+ </doc:scenario>
226
+ </doc:example>
227
+ */
228
+ angular.module('ngResource', ['ng']).
229
+ factory('$resource', ['$http', '$parse', function($http, $parse) {
230
+ var DEFAULT_ACTIONS = {
231
+ 'get': {method:'GET'},
232
+ 'save': {method:'POST'},
233
+ 'query': {method:'GET', isArray:true},
234
+ 'remove': {method:'DELETE'},
235
+ 'delete': {method:'DELETE'}
236
+ };
237
+ var noop = angular.noop,
238
+ forEach = angular.forEach,
239
+ extend = angular.extend,
240
+ copy = angular.copy,
241
+ isFunction = angular.isFunction,
242
+ getter = function(obj, path) {
243
+ return $parse(path)(obj);
244
+ };
245
+
246
+ /**
247
+ * We need our custom method because encodeURIComponent is too aggressive and doesn't follow
248
+ * http://www.ietf.org/rfc/rfc3986.txt with regards to the character set (pchar) allowed in path
249
+ * segments:
250
+ * segment = *pchar
251
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
252
+ * pct-encoded = "%" HEXDIG HEXDIG
253
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
254
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
255
+ * / "*" / "+" / "," / ";" / "="
256
+ */
257
+ function encodeUriSegment(val) {
258
+ return encodeUriQuery(val, true).
259
+ replace(/%26/gi, '&').
260
+ replace(/%3D/gi, '=').
261
+ replace(/%2B/gi, '+');
262
+ }
263
+
264
+
265
+ /**
266
+ * This method is intended for encoding *key* or *value* parts of query component. We need a custom
267
+ * method becuase encodeURIComponent is too agressive and encodes stuff that doesn't have to be
268
+ * encoded per http://tools.ietf.org/html/rfc3986:
269
+ * query = *( pchar / "/" / "?" )
270
+ * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
271
+ * unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
272
+ * pct-encoded = "%" HEXDIG HEXDIG
273
+ * sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
274
+ * / "*" / "+" / "," / ";" / "="
275
+ */
276
+ function encodeUriQuery(val, pctEncodeSpaces) {
277
+ return encodeURIComponent(val).
278
+ replace(/%40/gi, '@').
279
+ replace(/%3A/gi, ':').
280
+ replace(/%24/g, '$').
281
+ replace(/%2C/gi, ',').
282
+ replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
283
+ }
284
+
285
+ function Route(template, defaults) {
286
+ this.template = template = template + '#';
287
+ this.defaults = defaults || {};
288
+ var urlParams = this.urlParams = {};
289
+ forEach(template.split(/\W/), function(param){
290
+ if (param && (new RegExp("(^|[^\\\\]):" + param + "\\W").test(template))) {
291
+ urlParams[param] = true;
292
+ }
293
+ });
294
+ this.template = template.replace(/\\:/g, ':');
295
+ }
296
+
297
+ Route.prototype = {
298
+ url: function(params) {
299
+ var self = this,
300
+ url = this.template,
301
+ val,
302
+ encodedVal;
303
+
304
+ params = params || {};
305
+ forEach(this.urlParams, function(_, urlParam){
306
+ val = params.hasOwnProperty(urlParam) ? params[urlParam] : self.defaults[urlParam];
307
+ if (angular.isDefined(val) && val !== null) {
308
+ encodedVal = encodeUriSegment(val);
309
+ url = url.replace(new RegExp(":" + urlParam + "(\\W)", "g"), encodedVal + "$1");
310
+ } else {
311
+ url = url.replace(new RegExp("(\/?):" + urlParam + "(\\W)", "g"), function(match,
312
+ leadingSlashes, tail) {
313
+ if (tail.charAt(0) == '/') {
314
+ return tail;
315
+ } else {
316
+ return leadingSlashes + tail;
317
+ }
318
+ });
319
+ }
320
+ });
321
+ url = url.replace(/\/?#$/, '');
322
+ var query = [];
323
+ forEach(params, function(value, key){
324
+ if (!self.urlParams[key]) {
325
+ query.push(encodeUriQuery(key) + '=' + encodeUriQuery(value));
326
+ }
327
+ });
328
+ query.sort();
329
+ url = url.replace(/\/*$/, '');
330
+ return url + (query.length ? '?' + query.join('&') : '');
331
+ }
332
+ };
333
+
334
+
335
+ function ResourceFactory(url, paramDefaults, actions) {
336
+ var route = new Route(url);
337
+
338
+ actions = extend({}, DEFAULT_ACTIONS, actions);
339
+
340
+ function extractParams(data, actionParams){
341
+ var ids = {};
342
+ actionParams = extend({}, paramDefaults, actionParams);
343
+ forEach(actionParams, function(value, key){
344
+ ids[key] = value.charAt && value.charAt(0) == '@' ? getter(data, value.substr(1)) : value;
345
+ });
346
+ return ids;
347
+ }
348
+
349
+ function Resource(value){
350
+ copy(value || {}, this);
351
+ }
352
+
353
+ forEach(actions, function(action, name) {
354
+ action.method = angular.uppercase(action.method);
355
+ var hasBody = action.method == 'POST' || action.method == 'PUT' || action.method == 'PATCH';
356
+ Resource[name] = function(a1, a2, a3, a4) {
357
+ var params = {};
358
+ var data;
359
+ var success = noop;
360
+ var error = null;
361
+ switch(arguments.length) {
362
+ case 4:
363
+ error = a4;
364
+ success = a3;
365
+ //fallthrough
366
+ case 3:
367
+ case 2:
368
+ if (isFunction(a2)) {
369
+ if (isFunction(a1)) {
370
+ success = a1;
371
+ error = a2;
372
+ break;
373
+ }
374
+
375
+ success = a2;
376
+ error = a3;
377
+ //fallthrough
378
+ } else {
379
+ params = a1;
380
+ data = a2;
381
+ success = a3;
382
+ break;
383
+ }
384
+ case 1:
385
+ if (isFunction(a1)) success = a1;
386
+ else if (hasBody) data = a1;
387
+ else params = a1;
388
+ break;
389
+ case 0: break;
390
+ default:
391
+ throw "Expected between 0-4 arguments [params, data, success, error], got " +
392
+ arguments.length + " arguments.";
393
+ }
394
+
395
+ var value = this instanceof Resource ? this : (action.isArray ? [] : new Resource(data));
396
+ $http({
397
+ method: action.method,
398
+ url: route.url(extend({}, extractParams(data, action.params || {}), params)),
399
+ data: data
400
+ }).then(function(response) {
401
+ var data = response.data;
402
+
403
+ if (data) {
404
+ if (action.isArray) {
405
+ value.length = 0;
406
+ forEach(data, function(item) {
407
+ value.push(new Resource(item));
408
+ });
409
+ } else {
410
+ copy(data, value);
411
+ }
412
+ }
413
+ (success||noop)(value, response.headers);
414
+ }, error);
415
+
416
+ return value;
417
+ };
418
+
419
+
420
+ Resource.prototype['$' + name] = function(a1, a2, a3) {
421
+ var params = extractParams(this),
422
+ success = noop,
423
+ error;
424
+
425
+ switch(arguments.length) {
426
+ case 3: params = a1; success = a2; error = a3; break;
427
+ case 2:
428
+ case 1:
429
+ if (isFunction(a1)) {
430
+ success = a1;
431
+ error = a2;
432
+ } else {
433
+ params = a1;
434
+ success = a2 || noop;
435
+ }
436
+ case 0: break;
437
+ default:
438
+ throw "Expected between 1-3 arguments [params, success, error], got " +
439
+ arguments.length + " arguments.";
440
+ }
441
+ var data = hasBody ? this : undefined;
442
+ Resource[name].call(this, params, data, success, error);
443
+ };
444
+ });
445
+
446
+ Resource.bind = function(additionalParamDefaults){
447
+ return ResourceFactory(url, extend({}, paramDefaults, additionalParamDefaults), actions);
448
+ };
449
+
450
+ return Resource;
451
+ }
452
+
453
+ return ResourceFactory;
454
+ }]);
455
+
456
+
457
+ })(window, window.angular);