artifact 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,55 @@
1
+ require File.join(File.dirname(__FILE__), 'helpers')
2
+ %w(sinatra/base active_support/inflector json).each { |lib| require lib }
3
+
4
+ module Artifact
5
+
6
+ class App < Sinatra::Base
7
+
8
+ set :method_override, true
9
+ set :sessions, true
10
+ set :views, File.expand_path(File.join(File.dirname(__FILE__), 'views'))
11
+
12
+ use Rack::ShowExceptions
13
+ # use Rack::CommonLogger, LOGGER
14
+ # use Rack::Flash
15
+ use Rack::Static,
16
+ :urls => %w(/css /img /js),
17
+ :root => File.expand_path(File.join(File.dirname(__FILE__), 'public'))
18
+
19
+ helpers do
20
+ # include Rack::Utils # provides escape, parse_query and unescape
21
+ # include Sinatra::ContentFor
22
+ include Helpers
23
+ def logger; LOGGER; end
24
+ end
25
+
26
+ error do
27
+ if request.env['sinatra.error'].class.name['DocumentNotFound']
28
+ not_found
29
+ else
30
+ erb :'errors/500', :layout => false
31
+ end
32
+ end
33
+
34
+ not_found do
35
+ not_found
36
+ end
37
+
38
+ private
39
+
40
+ # FIXME: SOON!
41
+ def current_user
42
+ author = {
43
+ :email => "ofofofof@gmail.com",
44
+ :time => Time.now,
45
+ :name => "The User"
46
+ }
47
+ end
48
+
49
+ def not_found
50
+ erb :'errors/404', :layout => false
51
+ end
52
+
53
+ end
54
+
55
+ end
@@ -0,0 +1,60 @@
1
+ require File.join(File.dirname(__FILE__), 'app')
2
+
3
+ module Artifact
4
+
5
+ def self.files
6
+ @files ||= Tree.new(config.root)
7
+ end
8
+
9
+ class Files < App
10
+
11
+ get '/' do
12
+ redirect to('/files')
13
+ end
14
+
15
+ get '/files' do
16
+ @files = Artifact.files.all
17
+ view 'files/index'
18
+ end
19
+
20
+ get '/files/new' do
21
+ view 'files/new'
22
+ end
23
+
24
+ post '/files/create' do
25
+ @file = Artifact.files.new(params[:path])
26
+ @file.update(params[:content], params[:meta])
27
+ redirect to('/')
28
+ end
29
+
30
+ get '/files/edit/*' do
31
+ get_file
32
+ view 'files/edit'
33
+ end
34
+
35
+ post '/files/update/*' do
36
+ get_file
37
+ @file.update(params[:content], params[:meta])
38
+
39
+ if params[:submit] and params[:submit][/push/i]
40
+ return redirect to("/commit/#{params[:splat][0]}")
41
+ end
42
+
43
+ redirect to('/')
44
+ end
45
+
46
+ get '/commit/*' do
47
+ get_file
48
+ Artifact.repo.save(@file.full_path, current_user)
49
+ redirect to('/')
50
+ end
51
+
52
+ private
53
+
54
+ def get_file
55
+ Artifact.open_file(params[:splat][0]) or raise 'File not found!'
56
+ end
57
+
58
+ end
59
+
60
+ end
@@ -0,0 +1,130 @@
1
+ require 'uri'
2
+
3
+ module Artifact
4
+
5
+ module Helpers
6
+
7
+ #########################################
8
+ # main
9
+ #########################################
10
+
11
+ def app_name
12
+ 'Artifact'
13
+ end
14
+
15
+ def action
16
+ request.path_info.gsub('/','').empty? ? 'home' : request.path_info.gsub('/',' ')
17
+ end
18
+
19
+ def requires!(required, hash = params)
20
+ if required.is_a?(Hash)
21
+ required.each { |k, vals| requires!(vals, hash[k]) }
22
+ elsif hash.nil? or hash.is_a?(String) or required.any?{ |p| hash[p].nil? }
23
+ halt(400, 'Missing parameters.')
24
+ end
25
+ end
26
+
27
+ #########################################
28
+ # rendering
29
+ #########################################
30
+
31
+ def partial(name, *args)
32
+ partial_name = name.to_s["/"] ? name.to_s.reverse.sub("/", "_/").reverse : "_#{name}"
33
+ erb(partial_name.to_sym, {:layout => false}, *args)
34
+ end
35
+
36
+ def view(view_name, *args)
37
+ layout = request.xhr? ? false : @mobile_host ? :'layout.mobile' : true
38
+ erb(view_name.to_sym, {:layout => layout}, *args)
39
+ end
40
+
41
+ #########################################
42
+ # paths, links
43
+ #########################################
44
+
45
+ def base_path
46
+ env['SCRIPT_NAME']
47
+ end
48
+
49
+ def get_class_name(klass)
50
+ klass.name.to_s.split("::").last
51
+ end
52
+
53
+ def guess_model_path(m)
54
+ base_path + '/' + get_class_name(m).pluralize.downcase
55
+ end
56
+
57
+ def url_for(object)
58
+ object.nil? ? "#" : object.is_a?(String) ? object \
59
+ : object.respond_to?(:path) ? object.path : "#{guess_model_path(object.class)}/#{object.to_param}"
60
+ end
61
+
62
+ def link_to(string, resource)
63
+ "<a href='#{url_for(resource)}'>#{string}</a>"
64
+ end
65
+
66
+ def active?(link)
67
+ request.path[link] ? 'active' : ''
68
+ end
69
+
70
+ def view_upload_path(file)
71
+ url(upload_relative_path(file))
72
+ end
73
+
74
+ def upload_relative_path(file)
75
+ # we need to start with a /, otherwise middleman will insert his image_path as prefix
76
+ '/' + Artifact.config.uploads_path + file.path.sub(Artifact.uploads.path, '')
77
+ end
78
+
79
+ def is_image?(filename)
80
+ filename.match(/\.(png|jpg|jpeg|gif|bmp)$/i)
81
+ end
82
+
83
+ def post_preview_path(post)
84
+ str = post.path.sub(Artifact.config.source_root, '')
85
+ if Artifact.config.directory_indexes
86
+ str.sub(post.filename, post.filename.split('.').first)
87
+ end
88
+ end
89
+
90
+ def delete_link(resource, options = {})
91
+ css_class = options[:class] || ''
92
+ text = options[:text] || 'Delete'
93
+ path = url_for(resource)
94
+ '<form class="destroy" style="display:inline" method="post" action="'+ path +'" onsubmit="return confirm(\'Are you sure?\');">
95
+ <input type="hidden" name="_method" value="delete" />
96
+ <button type="submit" class="' + css_class +' btn btn-danger">' + text + '</button>
97
+ </form>'
98
+ end
99
+
100
+ def in_words(time)
101
+ minutes = (((Time.now - time).abs)/60).round
102
+ return nil if minutes < 0
103
+
104
+ case minutes
105
+ when 0..1 then 'less than a minute'
106
+ when 2..4 then 'less than 5 minutes'
107
+ when 5..14 then 'less than 15 minutes'
108
+ when 15..29 then "half an hour"
109
+ when 30..59 then "#{minutes} minutes"
110
+ when 60..119 then '1 hour'
111
+ when 120..239 then '2 hours'
112
+ when 240..479 then '4 hours'
113
+ when 480..719 then '8 hours'
114
+ when 720..1439 then '12 hours'
115
+ when 1440..11519 then "#{(minutes/1440).floor} days"
116
+ when 11520..43199 then "#{(minutes/11520).floor} weeks"
117
+ when 43200..525599 then "#{(minutes/43200).floor} months"
118
+ else "#{(minutes/525600).floor} years"
119
+ end
120
+ end
121
+
122
+ def time_ago_in_words(time)
123
+ if str = in_words(time)
124
+ "#{str} ago"
125
+ end
126
+ end
127
+
128
+ end
129
+
130
+ end
@@ -0,0 +1,213 @@
1
+ require File.join(File.dirname(__FILE__), 'app')
2
+ require 'active_support'
3
+
4
+ class Middleman::Sitemap::Store
5
+
6
+ alias_method :original_initialize, :initialize
7
+
8
+ def initialize(app)
9
+ Artifact.middleman = app
10
+ original_initialize(app)
11
+ end
12
+
13
+ end
14
+
15
+ class Middleman::CoreExtensions::FrontMatter
16
+
17
+ # extension's clear_data method doesn't work
18
+ # because it uses the full file path, while
19
+ # the cache setter uses only the relative path
20
+ def flush_cache!
21
+ @cache = {}
22
+ end
23
+
24
+ end
25
+
26
+ module Artifact
27
+
28
+ def self.middleman=(app)
29
+ @app = app
30
+ end
31
+
32
+ def self.middleman
33
+ @app
34
+ end
35
+
36
+ def self.source_root
37
+ File.join(config.root, config.source_root)
38
+ end
39
+
40
+ def self.posts
41
+ @posts ||= Tree.new(File.join(source_root, Artifact.config.posts_path))
42
+ end
43
+
44
+ def self.drafts
45
+ opts = {:new_file_path => ''} # flat, no date
46
+ @drafts ||= Tree.new(File.join(source_root, Artifact.config.drafts_path), opts)
47
+ end
48
+
49
+ def self.uploads
50
+ @uploads ||= Tree.new(File.join(source_root, Artifact.config.uploads_path))
51
+ end
52
+
53
+ class Middleman < App
54
+
55
+ POST_EXTENSION = '.html.markdown'
56
+
57
+ get '/' do
58
+ redirect to('/posts')
59
+ end
60
+
61
+ post '/build' do
62
+ # updated! and rebuild!
63
+ rebuild!
64
+ redirect to('/')
65
+ end
66
+
67
+ get '/search' do
68
+ if params[:q].present?
69
+ normalized = params[:q].gsub(' ', '-').downcase
70
+ @list = filter_and_sort(Artifact.posts.find(normalized), :date)
71
+ else
72
+ @list = filter_and_sort(Artifact.posts.all.last(10), :date)
73
+ end
74
+ partial 'posts/list'
75
+ end
76
+
77
+ get '/posts' do
78
+ @posts = filter_and_sort(Artifact.posts.all.last(10), :date)
79
+ @drafts = filter_and_sort(Artifact.drafts.all, :last_modified)
80
+ view 'posts/index'
81
+ end
82
+
83
+ get '/posts/new' do
84
+ view 'posts/new'
85
+ end
86
+
87
+ post '/posts' do
88
+ data = params[:meta] # includes title and maybe something else
89
+ filename = data[:title].parameterize
90
+
91
+ @file = Artifact.drafts.new(filename, POST_EXTENSION)
92
+ @file.update(params[:content], data)
93
+ redirect to('/posts')
94
+ end
95
+
96
+ get '/drafts/*' do
97
+ @post = MarkdownFile.new(params[:splat][0])
98
+ @draft = true
99
+ view 'posts/edit'
100
+ end
101
+
102
+ get '/posts/*' do
103
+ @post = MarkdownFile.new(params[:splat][0])
104
+ view 'posts/edit'
105
+ end
106
+
107
+ post '/posts/*' do
108
+ @post = MarkdownFile.new(params[:splat][0])
109
+
110
+ meta = (params[:meta] || {}).merge('last_updated_by' => current_user[:email])
111
+ @post.update(params[:content].gsub("\r\n", "\n"), meta)
112
+
113
+ if params[:save] # user clicked on 'save' or 'published'
114
+ # if draft was previously saved this will raise 'coz no changes.
115
+ Artifact.repo.save(@post.path, current_user, false)
116
+
117
+ if params[:save] == 'Publish'
118
+ @post.update_meta(:date, Time.now.utc.strftime("%Y-%m-%d %H:%M %Z"))
119
+ publish_post!(@post)
120
+ elsif params[:save] == 'Unpublish'
121
+ unpublish_post!(@post)
122
+ end
123
+ end
124
+
125
+ updated!
126
+ redirect to('/posts')
127
+ end
128
+
129
+ delete '/posts/*' do
130
+ @post = MarkdownFile.new(params[:splat][0])
131
+ @post.delete
132
+ Artifact.repo.remove(@post.path, current_user) if Artifact.repo.exists?(@post.path)
133
+ updated!
134
+
135
+ redirect to('/posts')
136
+ end
137
+
138
+ get '/uploads/*' do
139
+ file = params[:splat].first
140
+ send_file File.join(Artifact.uploads.path, file)
141
+ end
142
+
143
+ get '/uploads' do
144
+ get_uploads
145
+ # @uploads.select! {|i| i.match(/\.(png|jpg|jpeg|gif)/i) }
146
+ partial 'uploads/list'
147
+ end
148
+
149
+ post '/uploads' do
150
+ unless params[:file] &&
151
+ (tmpfile = params[:file][:tempfile]) &&
152
+ (name = params[:file][:filename])
153
+ raise 'No file uploaded.'
154
+ else
155
+ filename = File.basename(name, File.extname(name)).parameterize # without extension
156
+ file = Artifact.uploads.new(filename, File.extname(name))
157
+ file.update(tmpfile.read)
158
+ Artifact.repo.save(file.path, current_user) # pass relative path
159
+
160
+ updated!
161
+ redirect to('/')
162
+ end
163
+ end
164
+
165
+ private
166
+
167
+ def get_uploads
168
+ @uploads = Artifact.uploads.all.last(10).reverse
169
+ end
170
+
171
+ def filter_and_sort(posts, criteria)
172
+ posts.select { |f| f.is_a?(WritableFile) }.sort do |a,b|
173
+ b.send(criteria) <=> a.send(criteria)
174
+ end
175
+ end
176
+
177
+ def publish_post!(post)
178
+ filename = post.title.parameterize
179
+ new_file = Artifact.posts.new(filename, POST_EXTENSION)
180
+ Artifact.repo.move(post.path, new_file.path, current_user)
181
+ end
182
+
183
+ def unpublish_post!(post)
184
+ filename = post.title.parameterize
185
+ new_file = Artifact.drafts.new(filename, POST_EXTENSION)
186
+ Artifact.repo.move(post.path, new_file.path, current_user)
187
+ end
188
+
189
+ def rebuild!
190
+ puts `bundle exec middleman build`
191
+ end
192
+
193
+ def updated!
194
+ return if Artifact.middleman.nil? # not loaded yet!
195
+
196
+ puts "Reloading Middleman..."
197
+ Artifact.middleman.files.reload_path(Artifact.drafts.path)
198
+ Artifact.middleman.files.reload_path(Artifact.posts.path)
199
+ Artifact.middleman.files.reload_path(Artifact.uploads.path)
200
+
201
+ Artifact.middleman.cache.clear # Tilt (template) cache
202
+ # Artifact.middleman.sitemap.send(:reset_lookup_cache!)
203
+ Artifact.middleman.sitemap.rebuild_resource_list!
204
+
205
+ if Artifact.middleman.extensions[:frontmatter]
206
+ puts "Flushing frontmatter cache..."
207
+ Artifact.middleman.extensions[:frontmatter].flush_cache!
208
+ end
209
+ end
210
+
211
+ end
212
+
213
+ end