PageRenderTime 51ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/app/collections/files.js

https://gitlab.com/gregtyka/prose
JavaScript | 304 lines | 234 code | 48 blank | 22 comment | 39 complexity | 6ffa0b7c0544c00e0c002bbb138a224e MD5 | raw file
  1. var _ = require('underscore');
  2. var jsyaml = require('js-yaml');
  3. var queue = require('queue-async');
  4. var Backbone = require('backbone');
  5. var File = require('../models/file');
  6. var Folder = require('../models/folder');
  7. var cookie = require('../cookie');
  8. var util = require('../util');
  9. var ignore = require('ignore');
  10. module.exports = Backbone.Collection.extend({
  11. model: function(attributes, options) {
  12. // TODO: handle 'symlink' and 'submodule' type
  13. // TODO: coerce tree/folder to a single type
  14. switch(attributes.type) {
  15. case 'tree':
  16. return new Folder(attributes, options);
  17. break;
  18. case 'blob':
  19. return new File(attributes, options);
  20. break;
  21. default:
  22. return new File(attributes, options);
  23. break;
  24. }
  25. },
  26. initialize: function(models, options) {
  27. _.bindAll(this);
  28. this.repo = options.repo;
  29. this.branch = options.branch;
  30. this.sha = options.sha;
  31. // Sort files reverse alphabetically if path begins with '_posts/'
  32. this.comparator = function(a, b) {
  33. var typeA = a.get('type');
  34. var typeB = b.get('type');
  35. var pathA = a.get('path');
  36. var pathB = b.get('path');
  37. var regex = /^_posts\/.*$/
  38. if (typeA === typeB && typeA === 'file' && regex.test(pathA) && regex.test(pathB)) {
  39. // Reverse alphabetical
  40. return pathA < pathB ? 1 : -1;
  41. } else if (typeA === typeB) {
  42. // Alphabetical
  43. return pathA < pathB ? -1 : 1;
  44. } else {
  45. switch(typeA) {
  46. case 'tree':
  47. case 'folder':
  48. return -1;
  49. break;
  50. case 'file':
  51. return typeB === 'folder' || typeB === 'tree' ? 1 : -1;
  52. break;
  53. }
  54. }
  55. };
  56. },
  57. parse: function(resp, options) {
  58. return _.map(resp.tree, (function(file) {
  59. return _.extend(file, {
  60. branch: this.branch,
  61. collection: this,
  62. repo: this.repo
  63. })
  64. }).bind(this));
  65. },
  66. parseConfig: function(config, options) {
  67. var content = config.get('content');
  68. // Attempt to parse YAML
  69. try {
  70. config = jsyaml.safeLoad(content);
  71. } catch(err) {
  72. console.log("Error parsing YAML");
  73. console.log(err);
  74. }
  75. this.config = {};
  76. if (config && config.prose) {
  77. // Load _config.yml, set parsed value on collection
  78. // Extend to capture settings from outside config.prose
  79. // while allowing override
  80. this.config = _.extend(config, config.prose);
  81. if (config.prose.ignore) {
  82. this.parseIgnore(config.prose.ignore);
  83. }
  84. if (config.prose.metadata) {
  85. var metadata = config.prose.metadata;
  86. // Serial queue to not break global scope JSONP callbacks
  87. var q = queue(1);
  88. _.each(metadata, function(raw, key) {
  89. q.defer(function(cb) {
  90. var subq = queue();
  91. var defaults;
  92. if (_.isObject(raw)) {
  93. defaults = raw;
  94. _.each(defaults, function(value, key) {
  95. var regex = /^https?:\/\//;
  96. // Parse JSON URL values
  97. if (value && value.field && value.field.options &&
  98. _.isString(value.field.options) &&
  99. regex.test(value.field.options)) {
  100. subq.defer(function(cb) {
  101. $.ajax({
  102. cache: true,
  103. dataType: 'jsonp',
  104. jsonp: false,
  105. jsonpCallback: value.field.options.split('?callback=')[1] || 'callback',
  106. timeout: 5000,
  107. url: value.field.options,
  108. success: (function(d) {
  109. value.field.options = _.compact(d);
  110. cb();
  111. }).bind(this)
  112. });
  113. });
  114. }
  115. if (value && value.field && value.field.value === "CURRENT_DATETIME") {
  116. value.field.value = (new Date()).format('Y-m-d H:i O');
  117. }
  118. });
  119. } else if (_.isString(raw)) {
  120. try {
  121. defaults = jsyaml.safeLoad(raw);
  122. if (defaults.date === "CURRENT_DATETIME") {
  123. var current = (new Date()).format('Y-m-d H:i O');
  124. defaults.date = current;
  125. raw = raw.replace("CURRENT_DATETIME", current);
  126. }
  127. } catch(err) {
  128. console.log("Error parsing default values.");
  129. console.log(err);
  130. }
  131. }
  132. subq.awaitAll(function() {
  133. metadata[key] = defaults;
  134. cb();
  135. });
  136. });
  137. });
  138. q.awaitAll((function() {
  139. // Save parsed config to the collection as it's used accross
  140. // files of the same collection and shouldn't be re-parsed each time
  141. this.defaults = metadata;
  142. if (_.isFunction(options.success)) options.success.apply(this, options.args);
  143. }).bind(this));
  144. } else {
  145. if (_.isFunction(options.success)) options.success.apply(this, options.args);
  146. }
  147. } else {
  148. if (_.isFunction(options.success)) options.success.apply(this, options.args);
  149. }
  150. },
  151. parseIgnore: function(ignorePatterns) {
  152. var ignoreFilter = ignore().addPattern(ignorePatterns).createFilter();
  153. this.filteredModel = new Backbone.Collection(this.filter(function(file) {
  154. return ignoreFilter(file.id);
  155. }));
  156. },
  157. fetch: function(options) {
  158. options = _.clone(options) || {};
  159. var success = options.success;
  160. var args = options.args;
  161. Backbone.Collection.prototype.fetch.call(this, _.extend(options, {
  162. success: (function(model, res, options) {
  163. var config = this.findWhere({ path: '_prose.yml' }) ||
  164. this.findWhere({ path: '_config.yml' });
  165. if (config) {
  166. config.fetch({
  167. success: (function() {
  168. this.parseConfig(config, { success: success, args: args });
  169. }).bind(this)
  170. });
  171. } else {
  172. if (_.isFunction(success)) success.apply(this, args);
  173. }
  174. }).bind(this)
  175. }));
  176. },
  177. restore: function(file, options) {
  178. options = options ? _.clone(options) : {};
  179. var path = file.filename;
  180. var success = options.success;
  181. $.ajax({
  182. type: 'GET',
  183. url: file.contents_url,
  184. headers: {
  185. Accept: 'application/vnd.github.v3.raw'
  186. },
  187. success: (function(res) {
  188. // initialize new File model with content
  189. var model = new File({
  190. branch: this.branch,
  191. collection: this,
  192. content: res,
  193. path: path,
  194. repo: this.repo
  195. });
  196. var name = util.extractFilename(path)[1];
  197. model.set('placeholder', t('actions.commits.created', { filename: name }));
  198. // add to collection on save
  199. model.save({
  200. success: (function(model, res, options) {
  201. // Update model attributes and add to collection
  202. model.set(res.content);
  203. this.add(model);
  204. if (_.isFunction(success)) success(model, res, options);
  205. }).bind(this),
  206. error: options.error
  207. });
  208. }).bind(this),
  209. error: options.error
  210. });
  211. },
  212. upload: function(file, content, path, options) {
  213. var success = options.success;
  214. var extension = file.type.split('/').pop();
  215. var uid;
  216. if (!path) {
  217. uid = file.name;
  218. if (this.assetsDirectory) {
  219. path = this.assetsDirectory + '/' + uid;
  220. } else {
  221. path = this.model.path ? this.model.path + '/' + uid : uid;
  222. }
  223. }
  224. // If path matches an existing file, confirm the overwrite is intentional
  225. // then set new content and update the existing file
  226. var model = this.findWhere({ path: path });
  227. if (model) {
  228. // TODO: confirm overwrite with UI prompt
  229. model.set('content', content);
  230. model.set('placeholder', t('actions.commits.updated', { filename: file.name }));
  231. } else {
  232. // initialize new File model with content
  233. model = new File({
  234. branch: this.branch,
  235. collection: this,
  236. content: content,
  237. path: path,
  238. repo: this.repo
  239. });
  240. model.set('placeholder', t('actions.commits.created', { filename: file.name }));
  241. }
  242. // add to collection on save
  243. model.save({
  244. success: (function(model, res, options) {
  245. // Update model attributes and add to collection
  246. model.set(res.content);
  247. this.add(model);
  248. if (_.isFunction(success)) success(model, res, options);
  249. }).bind(this),
  250. error: options.error
  251. });
  252. },
  253. url: function() {
  254. return this.repo.url() + '/git/trees/' + this.sha + '?recursive=1';
  255. }
  256. });