distribute_tree 0.0.1

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.
Files changed (65) hide show
  1. data/.gitignore +21 -0
  2. data/Gemfile +1 -0
  3. data/Guardfile +14 -0
  4. data/LICENSE +20 -0
  5. data/README.markdown +76 -0
  6. data/app/assets/javascripts/distribute_tree.js +137 -0
  7. data/app/controllers/distribute_controller.rb +105 -0
  8. data/app/models/distribute_tree_status.rb +16 -0
  9. data/app/views/distribute/servers.html.haml +38 -0
  10. data/config/routes.rb +12 -0
  11. data/distribute_tree.gemspec +32 -0
  12. data/lib/distribute_tree.rb +104 -0
  13. data/lib/rails_engine.rb +16 -0
  14. data/spec/dummy/.gitignore +15 -0
  15. data/spec/dummy/Gemfile +38 -0
  16. data/spec/dummy/README.rdoc +261 -0
  17. data/spec/dummy/Rakefile +7 -0
  18. data/spec/dummy/app/assets/images/rails.png +0 -0
  19. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  20. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  21. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  22. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  23. data/spec/dummy/app/mailers/.gitkeep +0 -0
  24. data/spec/dummy/app/models/.gitkeep +0 -0
  25. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  26. data/spec/dummy/config.ru +4 -0
  27. data/spec/dummy/config/application.rb +62 -0
  28. data/spec/dummy/config/boot.rb +6 -0
  29. data/spec/dummy/config/database.yml +25 -0
  30. data/spec/dummy/config/environment.rb +5 -0
  31. data/spec/dummy/config/environments/development.rb +37 -0
  32. data/spec/dummy/config/environments/production.rb +67 -0
  33. data/spec/dummy/config/environments/test.rb +37 -0
  34. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  35. data/spec/dummy/config/initializers/inflections.rb +15 -0
  36. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  37. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  38. data/spec/dummy/config/initializers/session_store.rb +8 -0
  39. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  40. data/spec/dummy/config/locales/en.yml +5 -0
  41. data/spec/dummy/config/routes.rb +58 -0
  42. data/spec/dummy/db/seeds.rb +7 -0
  43. data/spec/dummy/doc/README_FOR_APP +2 -0
  44. data/spec/dummy/lib/assets/.gitkeep +0 -0
  45. data/spec/dummy/lib/tasks/.gitkeep +0 -0
  46. data/spec/dummy/log/.gitkeep +0 -0
  47. data/spec/dummy/public/404.html +26 -0
  48. data/spec/dummy/public/422.html +26 -0
  49. data/spec/dummy/public/500.html +25 -0
  50. data/spec/dummy/public/favicon.ico +0 -0
  51. data/spec/dummy/public/index.html +241 -0
  52. data/spec/dummy/public/robots.txt +5 -0
  53. data/spec/dummy/script/rails +6 -0
  54. data/spec/dummy/test/fixtures/.gitkeep +0 -0
  55. data/spec/dummy/test/functional/.gitkeep +0 -0
  56. data/spec/dummy/test/integration/.gitkeep +0 -0
  57. data/spec/dummy/test/performance/browsing_test.rb +12 -0
  58. data/spec/dummy/test/test_helper.rb +13 -0
  59. data/spec/dummy/test/unit/.gitkeep +0 -0
  60. data/spec/dummy/vendor/assets/javascripts/.gitkeep +0 -0
  61. data/spec/dummy/vendor/assets/stylesheets/.gitkeep +0 -0
  62. data/spec/dummy/vendor/plugins/.gitkeep +0 -0
  63. data/spec/spec_helper.rb +39 -0
  64. data/spec/support/mongoid.rb +5 -0
  65. metadata +287 -0
@@ -0,0 +1,21 @@
1
+ *.rbc
2
+ *.sassc
3
+ .sass-cache
4
+ capybara-*.html
5
+ .rspec
6
+ .rvmrc
7
+ /.bundle
8
+ /vendor/bundle
9
+ /log/*
10
+ /tmp/*
11
+ /db/*.sqlite3
12
+ /public/system/*
13
+ /coverage/
14
+ /spec/tmp/*
15
+ **.orig
16
+ rerun.txt
17
+ pickle-email-*.html
18
+ .project
19
+ config/initializers/secret_token.rb
20
+
21
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1 @@
1
+ gemspec
@@ -0,0 +1,14 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :test do
5
+ watch(%r{^lib/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
6
+ watch(%r{^test/.+_test\.rb$})
7
+ watch('test/test_helper.rb') { "test" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/models/(.+)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" }
11
+ watch(%r{^app/controllers/(.+)\.rb$}) { |m| "test/functional/#{m[1]}_test.rb" }
12
+ watch(%r{^app/views/.+\.rb$}) { "test/integration" }
13
+ watch('app/controllers/application_controller.rb') { ["test/functional", "test/integration"] }
14
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 SunshineLibrary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,76 @@
1
+ distribute_tree
2
+ ===============
3
+ 用于实现单个Cloud和多个Local服务器之间数据共享的 Rails Engine 。
4
+
5
+ Cloud(1) <-> Local(n) 同步机制概述
6
+ ---------------
7
+ 同步模式:
8
+
9
+ * 自动: 保存一个资源均只同步自身的JSON和File,即不递归同步子集。
10
+ * 手动: 新增一个学校,或者只想给某些学校,分配某些资源,在后台管理点击同步,并递归子集。
11
+
12
+ 启动方式
13
+ ---------------
14
+
15
+ ```bash
16
+ RAILS_ENV=production bundle exec rake resque:work QUEUE='cloud_distribute_tree' --trace
17
+ ```
18
+
19
+ 如果需要对一个queue起多个work,那么多开几个rake进程就好了,系统会自动排队和并发处理的。
20
+
21
+ [循环sync] local(只要在这里把资源标上创建地点即可让cloud识别) -> cloud -> locals
22
+
23
+ 同步架构变迁历史概述
24
+ ---------------
25
+ #### 第一代(mysql replication)
26
+
27
+ 优点:
28
+
29
+ 1. 从数据库层面一致。
30
+
31
+ 缺点:
32
+
33
+ 1. 单向。
34
+ 1. 无法提供细力度控制和外部操作。
35
+
36
+ #### 第二代(rabbitmq)
37
+
38
+ 优点:
39
+
40
+ 1. RabbitMQ高性能
41
+ 2. 引入基于JSON格式的API在不同系统间同步,并分成元数据和File两种数据类型分别分发。
42
+
43
+ 缺点:
44
+
45
+ 1. 按订阅全部同步,容易造成VPN网络堵塞。虽然可以改成点对点发送,但也失去了用rabbitmq的必要。
46
+ 2. 无细力度的UI,不透明。这个开发难度比 resque 高,且是另外一套系统。
47
+ 3. 无法管理细力度的失败的同步。
48
+
49
+ #### 第三代(resque)
50
+ 架构:基于resque队列系统。队列可视化,rails风格。从优点上继承了上代的RabbitMQ架构。
51
+
52
+ A. 初始化同步,或者中途中断后同步, cloud -> local。
53
+
54
+ 配置学校,可以按school同步,并在resque里递归从属或关联子集,产生新队列(这一想法基于之前给刘聪做的递归更新父级时间戳 [mongoid_touch_parents_recursively](https://github.com/SunshineLibrary/mongoid_touch_parents_recursively) 。
55
+
56
+ B. 增量更新同步. cloud -> local (lesson) ,或者local -> cloud (piece) 。
57
+
58
+ 在数据同步后紧接着触发文件同步,都放在resque里。两者交互机制就是经典的client-server API 架构CRUD数据,这样也就符合了我来书屋面试前还没成型的想法,隐约地想要把学校local也看成是cloud的伪android客户端。删除也参考warpgate的做法。
59
+
60
+ 注: 感谢和小平的冲突,感谢和诺哥的讨论,在萨莉亚吃完晚饭后,萦绕的思绪突然激发了这一想法。这个完全不是时间逼的。
61
+
62
+ FAQ
63
+ ---------------
64
+ 问题: paperclip包含的静态资源过大,比如100M,在cloud的resque连接上local的Rails接口去上传文件时会发生网络请求超时错误,并会占用一个unicorn slave。
65
+
66
+ 答案: 可以反过来做,改为在元数据同步过去到local后,不管paperclip包含的文件对象大小,
67
+ 让local自己主动向cloud的nginx静态资源服务器读取大文件,并在下载完成后保存即可,从而达到异步的效果。
68
+ 这点warpgate就是这样做的,跨网络传输文件还是下行快一点。
69
+
70
+ TODO
71
+ ---------------
72
+ 1. 突破Mongoid.relations限制以支持ActiveModel
73
+
74
+ 相关引用
75
+ ---------------
76
+ 1. [warpgate](https://github.com/SunshineLibrary/warpgate)
@@ -0,0 +1,137 @@
1
+ // 添加分发按钮到资源*表格*的每一项,并显示同步状态。
2
+ // =======================================
3
+ //
4
+ // 只在cloud开启该分发按钮
5
+ //
6
+ // =======================================
7
+ // ### 使用方法 ###
8
+ //
9
+ // 在table加上属性model_name,以及在各tr的首个td里加上uuid和name两个属性。
10
+ //
11
+ // %table.{model_name: :App}
12
+ // %tr
13
+ // %th.span5 UUID
14
+ // %th.span3= t "App.name"
15
+ // ...
16
+ // - @apps.each_with_index do |app, idx|
17
+ // %tr
18
+ // %td{uuid: app.uuid, name: app.name}= app.uuid
19
+ // ...
20
+ //
21
+ // TODO 载入resque是否正常服务
22
+ // TODO 加入验证用户
23
+ //
24
+ $(document).ready(function() {
25
+
26
+ var table = $("table[model_name]");
27
+ if (table.length === 0) { return false; } // 如果没有配置
28
+ if (table.length >= 2) { alert("目前distribute_tree.js只支持单页同步一种资源类型"); return false; }
29
+ var table_model_name = table.attr('model_name');
30
+
31
+ // Constant definement
32
+ var api_prefix = "/distribute_tree";
33
+
34
+ // 只有管理员才能同步学校
35
+ // TODO optimize
36
+ if (!$(".navbar ul.nav.pull-right li a").text().match(/陈大伟/) && (table_model_name === 'School')) { return false; }
37
+
38
+ // 1. 初始化页面元素
39
+ //
40
+ // 1.1 th 分发按钮
41
+ table.find('tr:first').append(
42
+ $("<th>").addClass("pointer span1").html(
43
+ $("<span>").addClass("icon icon-th")
44
+ )
45
+ // 并绑定 选择学校列表 弹框
46
+ ).on('click', 'th:last', function(event) {
47
+ var uuids = _.map(trs.find('td:last input:checked'), function(input) { return $(input).attr('uuid'); });
48
+ if (uuids.length === 0) { alert("请选择至少一个资源用于同步"); event.preventDefault(); return false; }
49
+ var modal_html = function() { return distribute_to_schools_modal.find('.modal'); };
50
+
51
+ function show() {
52
+ // 统计显示勾选的资源数目
53
+ modal_html().find('.modal-header h4.title span.count').html(uuids.length);
54
+ modal_html().modal('show');
55
+ }
56
+
57
+ // 判断是否ajax载入学校列表
58
+ if (_.isEmpty($("#distribute_to_schools_modal").html())) {
59
+ $.get(api_prefix + "/distribute/servers.html", function(html) {
60
+ distribute_to_schools_modal.html(html);
61
+
62
+ // 2. 绑定**提交事件**
63
+ distribute_to_schools_modal.on('click', 'button.submit', function() {
64
+ var school_uuids = _.map(distribute_to_schools_modal.find('table tr:gt(0) td input:checked'), function(input) { return $(input).attr('id'); });
65
+ if (school_uuids.length === 0) { alert("请选择至少一个学校用于同步"); event.preventDefault(); return false; }
66
+
67
+ // 2.1 处理元素显示
68
+ var btn = distribute_to_schools_modal.find(".modal-footer button.save-button");
69
+ btn.button('loading');
70
+
71
+ // 2.2 ajax提交数据
72
+ var data = {
73
+ model_name: table_model_name,
74
+ uuids: uuids,
75
+ school_uuids: school_uuids
76
+ };
77
+ // 2.3 服务器状态返回
78
+ $.post(api_prefix + '/distribute', data, function(e) {
79
+ var alert_html = modal_html().find(".alert");
80
+ alert_html.find('div.alert').removeClass('alert-success').removeClass('alert-error').addClass('alert-' + e.status);
81
+ alert_html.find('#flash_notice span.status').html('已经同步' + uuids.length + '个资源到' + school_uuids.length + '个服务器。');
82
+ $(".container.content").prepend(alert_html);
83
+ }).always(function(e) {
84
+ modal_html().modal('hide');
85
+ btn.button('reset');
86
+ });
87
+ });
88
+ // 并浮现
89
+ show();
90
+ });
91
+ } else {
92
+ show();
93
+ }
94
+ });
95
+ // 1.2 td 是否分发该资源
96
+ var trs = table.find('tr:gt(0)');
97
+ var is_defined_uuid_and_name_attrs = false; // check setup
98
+ _.each(trs, function(_tr) {
99
+ var tr = $(_tr);
100
+ var td_first = tr.find('td[uuid]');
101
+ if (td_first.length !== 0) { is_defined_uuid_and_name_attrs = true; }
102
+ tr.append(
103
+ $("<td>").append(
104
+ $("<input>").attr("type", "checkbox")
105
+ .attr('uuid', td_first.attr('uuid'))
106
+ .attr('name', td_first.attr('name'))
107
+ )
108
+ );
109
+ });
110
+ if (!is_defined_uuid_and_name_attrs) {
111
+ alert("没有定义在每行记录定义uuid和name,请察看distribute_tree.js的文档。");
112
+ event.preventDefault(); return false;
113
+ }
114
+ // 1.3 放置一个 学校列表容器
115
+ var distribute_to_schools_modal = $("body").append(
116
+ $("<div>").attr("id", "distribute_to_schools_modal")
117
+ ).find('#distribute_to_schools_modal');
118
+
119
+ // 1.4 载入各个资源的同步状态
120
+ var params = {model_name: table_model_name, uuids: _.map(trs.find('td:last input'), function(input) { return $(input).attr('uuid'); })};
121
+ $.get(api_prefix + "/distribute.json", params, function(result) {
122
+ _.each(trs, function(_tr) {
123
+ var td_last = $(_tr).find('td:last');
124
+ var urls = result.data[td_last.find('input[type=checkbox]').attr('uuid')];
125
+ // 如果存在*没有分发完成*的记录
126
+ if (urls) {
127
+ td_last.append($("<div>").addClass('pointer server_urls').text(urls.length))
128
+ .append($("<div>").addClass('hide server_urls count').text(urls.join(', ')));
129
+ }
130
+ });
131
+ // 显示具体分发地址
132
+ trs.on('click', 'td:last .server_urls', function(e) {
133
+ $(e.target).closest('td').find('.server_urls.count').toggle();
134
+ });
135
+ });
136
+
137
+ });
@@ -0,0 +1,105 @@
1
+ # encoding: UTF-8
2
+ class DistributeController < ApplicationController
3
+
4
+ #######################
5
+ ##### A. creator ####
6
+ #######################
7
+
8
+ def create
9
+ error_messages = []
10
+ error_messages << "Missing model name" if params[:model_name].blank?
11
+ error_messages << "Missing uuid" if params[:uuids].blank?
12
+ error_messages << "Please select an school" if params[:school_uuids].blank?
13
+
14
+ if not error_messages.blank?
15
+ render json: {status: "error", messages: error_messages}, status: :bad_request; return false
16
+ end
17
+
18
+ server_urls = School.where(uuid: {"$in" => params[:school_uuids]}).map(&:server_url)
19
+
20
+ params[:uuids].to_a.each do |uuid|
21
+ item = params[:model_name].classify.safe_constantize.uuid(uuid)
22
+ item.distribute_with_children(server_urls)
23
+ end
24
+
25
+ render json: {status: "success", messages: ["已加入到发送队列"]}
26
+ end
27
+
28
+ def servers
29
+ @schools = School.asc(:_id).where(is_enable_sync: true)
30
+ render layout: false
31
+ end
32
+
33
+ def index
34
+ data = DistributeTreeStatus.where(
35
+ item_class: params[:model_name],
36
+ item_uuid: {"$in" => params[:uuids]},
37
+ ).to_a.inject({}) do |h, status|
38
+ h[status.item_uuid] ||= []
39
+ h[status.item_uuid] << status.server_url
40
+ h
41
+ end
42
+
43
+ render json: {status: 'success', data: data }
44
+ end
45
+
46
+ # TODO
47
+ def paperclip_file
48
+ params[:model_name].constantize
49
+ end
50
+
51
+ #######################
52
+ ##### B. receiver ####
53
+ #######################
54
+
55
+ def receive
56
+ payload = params[:payload]
57
+
58
+ unless payload and payload[:model_name]
59
+ render json: {status: 'error', message: "payload and model_name required"}
60
+ return
61
+ end
62
+
63
+ model = payload[:model_name].classify().constantize
64
+
65
+ uuid = nil
66
+ if payload[:model] and payload[:model].is_a?(Hash)
67
+ uuid = payload[:model][:uuid]
68
+ end
69
+
70
+ unless model and uuid
71
+ render json: {status: 'error', message: "model and uuid not identified"}
72
+ return
73
+ end
74
+
75
+ model = model.unscoped # 兼容被删除资源
76
+
77
+ if payload[:model][:delete]
78
+ model.where(uuid: uuid).destroy_all
79
+ render json: {status: 'success', message: "model deleted"}
80
+ return
81
+ end
82
+
83
+ if model.where(uuid: uuid).exists?
84
+ # update, skip validation
85
+ item = model.uuid(uuid)
86
+ elsif [Piece, Image].include? model
87
+ # Local不自动创建Piece和Image, 只更新老师添加的
88
+ render json: {status: 'success'}
89
+ return
90
+ else
91
+ item = model.new
92
+ end
93
+
94
+ payload[:model].delete '_id'
95
+
96
+ item.assign_attributes payload[:model], without_protection: true
97
+ item.deleted_at = nil if payload[:model][:deleted_at].blank? # 恢复删除。也许assign_attributes payload[:model]这里面已经包含了。
98
+ if item.save validate: false
99
+ render json: {status: 'success'}
100
+ else
101
+ render json: {status: 'error', message: "fail to create or update model"}
102
+ end
103
+ end
104
+
105
+ end
@@ -0,0 +1,16 @@
1
+ # encoding: UTF-8
2
+
3
+ class DistributeTreeStatus
4
+ include Mongoid::Document; self.store_in database: self.name.underscore
5
+ include Mongoid::Timestamps
6
+
7
+ field :item_class, type: String
8
+ field :item_uuid, type: String
9
+ field :server_url, type: String
10
+
11
+ index({item_class: 1, item_uuid: 1, server_url: 1}, {background: true})
12
+
13
+ def self.insert item_class, item_uuid, server_url; DistributeTreeStatus.find_or_create_by(item_class: item_class, item_uuid: item_uuid, server_url: server_url) end
14
+ def self.delete item_class, item_uuid, server_url; DistributeTreeStatus.where(item_class: item_class, item_uuid: item_uuid, server_url: server_url).delete_all end
15
+
16
+ end
@@ -0,0 +1,38 @@
1
+ .modal.hide.fade{ "aria-hidden" => "true", :role => "dialog", :tabindex => "-1"}
2
+ %ul.items
3
+
4
+ .modal-header
5
+ %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} ×
6
+ %h4.title
7
+ %span 分发
8
+ %span.count
9
+ %span 个资源到下列勾选服务器
10
+
11
+ .modal-body
12
+ = form_tag({controller: "distribute", action: "create"}, method: "post", class: "form-horizontal") do
13
+ %div
14
+ %table.table.table-striped
15
+ %tbody
16
+ %tr
17
+ %th.span5.uuid.hide UUID
18
+ %th.span3 服务器
19
+ // %th.span2 服务器状态
20
+ %th.span2 分发
21
+ - @schools.each do |school|
22
+ %tr
23
+ %td.uuid.hide= school.uuid
24
+ %td= link_to school.name, school_path(school.uuid)
25
+ %td= check_box_tag(school.uuid)
26
+
27
+ .modal-footer
28
+ %button.submit.btn.btn-primary{:class => 'save-button', "data-loading-text" => "分发中…" } 分发
29
+ %button.btn{"aria-hidden" => "true", "data-dismiss" => "modal"} 取消
30
+
31
+ .alert.hide
32
+ %div.alert
33
+ %a.close{'data-dismiss' => 'alert'} x
34
+ %div#flash_notice
35
+ %span.status
36
+ %span 请前往
37
+ = link_to "同步队列", "/resque/queues/#{Mongoid::DistributeTree.default_queue}"
38
+ %span 察看最新状态
@@ -0,0 +1,12 @@
1
+ # encoding: UTF-8
2
+
3
+ Rails.application.routes.draw do
4
+
5
+ namespace :distribute_tree do
6
+ resources :distribute do
7
+ get :servers, :paperclip_file
8
+ post :receive
9
+ end
10
+ end
11
+
12
+ end