mirror of
https://github.com/samsonjs/samhuri.net.git
synced 2026-03-25 09:05:47 +00:00
Exorcise harp and node.js, server, and add Swift plan
This commit is contained in:
parent
309c7dddc0
commit
1e6348dbde
26 changed files with 81 additions and 10016 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,6 +1 @@
|
|||
.bundle
|
||||
node_modules
|
||||
www
|
||||
server/auth.json
|
||||
server/test-blog*
|
||||
origin-updated
|
||||
|
|
|
|||
43
.snyk
43
.snyk
|
|
@ -1,43 +0,0 @@
|
|||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.7.1
|
||||
ignore: {}
|
||||
# patches apply the minimum changes required to fix a vulnerability
|
||||
patch:
|
||||
'npm:ejs:20161128':
|
||||
- harp > terraform > ejs:
|
||||
patched: '2017-04-21T04:58:35.183Z'
|
||||
'npm:marked:20170112':
|
||||
- harp > terraform > marked:
|
||||
patched: '2017-04-21T04:58:35.183Z'
|
||||
'npm:negotiator:20160616':
|
||||
- harp > connect > compression > accepts > negotiator:
|
||||
patched: '2017-04-21T04:58:35.183Z'
|
||||
- harp > connect > serve-index > accepts > negotiator:
|
||||
patched: '2017-04-21T04:58:35.183Z'
|
||||
'npm:tar:20151103':
|
||||
- harp > download-github-repo > download > decompress > tar:
|
||||
patched: '2017-04-21T04:58:35.183Z'
|
||||
'npm:uglify-js:20151024':
|
||||
- harp > terraform > jade > transformers > uglify-js:
|
||||
patched: '2017-04-21T04:58:35.183Z'
|
||||
'npm:debug:20170905':
|
||||
- harp > connect > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > connect > express-session > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > connect > finalhandler > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > connect > morgan > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > connect > serve-index > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > send > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > connect > body-parser > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > connect > compression > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > connect > connect-timeout > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
- harp > connect > serve-static > send > debug:
|
||||
patched: '2017-09-29T03:22:08.982Z'
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
exclude = "{$exclude,www,node_modules,tweets,wayback,test-blog*}"
|
||||
exclude = "{$exclude,www,tweets,wayback}"
|
||||
include = "{$include,.gitignore}"
|
||||
|
|
|
|||
4
Gemfile
4
Gemfile
|
|
@ -6,7 +6,3 @@ gem 'htmlentities', '~> 4.3'
|
|||
gem 'mustache', '~> 1.0'
|
||||
gem 'nokogiri', '~> 1.10'
|
||||
gem 'rdiscount', '~> 2.1'
|
||||
gem 'sinatra', '~> 1.4'
|
||||
|
||||
gem 'rspec', '~> 3.3'
|
||||
gem 'guard-rspec', '~> 4.6'
|
||||
|
|
|
|||
63
Gemfile.lock
63
Gemfile.lock
|
|
@ -4,73 +4,15 @@ GEM
|
|||
addressable (2.5.2)
|
||||
public_suffix (>= 2.0.2, < 4.0)
|
||||
builder (3.2.3)
|
||||
coderay (1.1.2)
|
||||
css_parser (1.6.0)
|
||||
addressable
|
||||
diff-lcs (1.3)
|
||||
ffi (1.11.2)
|
||||
formatador (0.2.5)
|
||||
guard (2.14.2)
|
||||
formatador (>= 0.2.4)
|
||||
listen (>= 2.7, < 4.0)
|
||||
lumberjack (>= 1.0.12, < 2.0)
|
||||
nenv (~> 0.1)
|
||||
notiffany (~> 0.0)
|
||||
pry (>= 0.9.12)
|
||||
shellany (~> 0.0)
|
||||
thor (>= 0.18.1)
|
||||
guard-compat (1.2.1)
|
||||
guard-rspec (4.7.3)
|
||||
guard (~> 2.1)
|
||||
guard-compat (~> 1.1)
|
||||
rspec (>= 2.99.0, < 4.0)
|
||||
htmlentities (4.3.4)
|
||||
listen (3.1.5)
|
||||
rb-fsevent (~> 0.9, >= 0.9.4)
|
||||
rb-inotify (~> 0.9, >= 0.9.7)
|
||||
ruby_dep (~> 1.2)
|
||||
lumberjack (1.0.12)
|
||||
method_source (0.9.0)
|
||||
mini_portile2 (2.4.0)
|
||||
mustache (1.0.5)
|
||||
nenv (0.3.0)
|
||||
nokogiri (1.10.4)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
notiffany (0.1.1)
|
||||
nenv (~> 0.1)
|
||||
shellany (~> 0.0)
|
||||
pry (0.11.3)
|
||||
coderay (~> 1.1.0)
|
||||
method_source (~> 0.9.0)
|
||||
public_suffix (3.0.1)
|
||||
rack (1.6.11)
|
||||
rack-protection (1.5.5)
|
||||
rack
|
||||
rb-fsevent (0.10.2)
|
||||
rb-inotify (0.9.10)
|
||||
ffi (>= 0.5.0, < 2)
|
||||
rdiscount (2.2.0.1)
|
||||
rspec (3.7.0)
|
||||
rspec-core (~> 3.7.0)
|
||||
rspec-expectations (~> 3.7.0)
|
||||
rspec-mocks (~> 3.7.0)
|
||||
rspec-core (3.7.1)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-expectations (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-mocks (3.7.0)
|
||||
diff-lcs (>= 1.2.0, < 2.0)
|
||||
rspec-support (~> 3.7.0)
|
||||
rspec-support (3.7.0)
|
||||
ruby_dep (1.5.0)
|
||||
shellany (0.0.1)
|
||||
sinatra (1.4.8)
|
||||
rack (~> 1.5)
|
||||
rack-protection (~> 1.4)
|
||||
tilt (>= 1.3, < 3)
|
||||
thor (0.20.0)
|
||||
tilt (2.0.8)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
|
@ -78,13 +20,10 @@ PLATFORMS
|
|||
DEPENDENCIES
|
||||
builder (~> 3.2)
|
||||
css_parser (~> 1.3)
|
||||
guard-rspec (~> 4.6)
|
||||
htmlentities (~> 4.3)
|
||||
mustache (~> 1.0)
|
||||
nokogiri (~> 1.10)
|
||||
rdiscount (~> 2.1)
|
||||
rspec (~> 3.3)
|
||||
sinatra (~> 1.4)
|
||||
|
||||
BUNDLED WITH
|
||||
1.16.1
|
||||
1.17.1
|
||||
|
|
|
|||
15
Makefile
15
Makefile
|
|
@ -1,9 +1,5 @@
|
|||
all: compile
|
||||
|
||||
rss:
|
||||
@echo
|
||||
ruby -w ./bin/rss.rb public
|
||||
|
||||
compile:
|
||||
@echo
|
||||
./bin/compile.sh .
|
||||
|
|
@ -16,13 +12,4 @@ publish_beta: compile
|
|||
@echo
|
||||
./bin/publish.sh --beta --delete
|
||||
|
||||
test_blog:
|
||||
./bin/create-test-blog.sh server/test-blog
|
||||
|
||||
clean:
|
||||
rm -rf server/test-blog server/test-blog-origin.git
|
||||
|
||||
spec:
|
||||
cd server && rspec -f documentation
|
||||
|
||||
.PHONY: rss compile publish publish_beta test_blog clean spec
|
||||
.PHONY: compile publish publish_beta
|
||||
|
|
|
|||
53
Readme.md
Normal file
53
Readme.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# samhuri.net
|
||||
|
||||
The source code for [samhuri.net](https://samhuri.net).
|
||||
|
||||
# New version using Swift
|
||||
|
||||
The idea is to create a bespoke set of tools, not a general thing like Jekyll. If something can be factored out later that's a bonus, not a goal.
|
||||
|
||||
This is a plan for migrating from a mix of node.js (harp) and Ruby to Swift. Use Ink, John Sundell's Markdown parser, to render posts, and some other library for generating HTML. Will probably try generating HTML from code because I've never tried it and it seems fun. The pointfree.com guys have one already and Sundell is releasing one Real Soon(tm).
|
||||
|
||||
This version will go back to its roots and use headers at the top of markdown files. It was so much easier than indexing everything. Check out [9af9d75][] to see how nice it was back in the day. Perhaps it would even make sense to port that old version to Swift rather than working with the current version. Migrate the last few years' worth of posts over by hand since there isn't a lot there.
|
||||
|
||||
[9af9d75]: https://github.com/samsonjs/samhuri.net/tree/9af9d75565133104beb54f1bfdd3d4efe3e16982
|
||||
|
||||
Execution, trying TDD for the first time:
|
||||
|
||||
- [ ] Replace harp with custom Swift code
|
||||
|
||||
- [ ] Write a test harness that renders a site and then checks the output with `diff -r`
|
||||
|
||||
- [ ] Copy source files to their destination without transformations
|
||||
|
||||
- [ ] Transform files as needed:
|
||||
|
||||
- [ ] LESS -> CSS
|
||||
|
||||
- [ ] ? -> HTML
|
||||
|
||||
- [ ] \(decide if ? is code or templates (maybe Stencils?))
|
||||
|
||||
- [ ] Port .ejs templates to the new thing
|
||||
|
||||
- [ ] Add a link to the code for samhuri.net somewhere ... so meta (about page?)
|
||||
|
||||
- [ ] Migrate posts to markdown with headers somehow
|
||||
|
||||
- [ ] Decide whether to migrate from [9af9d75][] or the current harp format (probably easier to migrate the old format)
|
||||
|
||||
- [ ] Migrate posts
|
||||
|
||||
- [ ] Generate RSS feed (ditch mustache templates)
|
||||
|
||||
- [ ] Generate JSON feed
|
||||
|
||||
- [ ] Munge HTML files to make them available without an extension (index.html hack)
|
||||
|
||||
- [ ] Inline CSS?
|
||||
|
||||
- [ ] Minify JS
|
||||
|
||||
- [ ] Add a server for local use and simple production setups
|
||||
|
||||
- [ ] Figure out an iPad workflow with minimal code. Maybe a small app with some extensions and shortcuts?
|
||||
|
|
@ -3,14 +3,14 @@
|
|||
# bail on errors
|
||||
set -e
|
||||
|
||||
export PATH="$HOME/.rbenv/shims:$PATH"
|
||||
# export PATH="$HOME/.rbenv/shims:$PATH"
|
||||
|
||||
echo "*** bootstrap samhuri.net"
|
||||
|
||||
echo "* bundle install"
|
||||
bundle install
|
||||
|
||||
echo "* npm install"
|
||||
npm install
|
||||
# echo "* npm install"
|
||||
# npm install
|
||||
|
||||
echo "*** done"
|
||||
|
|
|
|||
|
|
@ -3,50 +3,33 @@
|
|||
# bail on errors
|
||||
set -e
|
||||
|
||||
export PATH="$HOME/.rbenv/shims:$PATH"
|
||||
# export PATH="$HOME/.rbenv/shims:$PATH"
|
||||
|
||||
DIR=$(dirname "$0")
|
||||
HARP="node_modules/harp/bin/harp"
|
||||
# HARP="node_modules/harp/bin/harp"
|
||||
BLOG_DIR="${1:-${DIR}/..}"
|
||||
TARGET="${BLOG_DIR%/}/${2:-www}"
|
||||
LOCK_FILE="$BLOG_DIR/compile.lock"
|
||||
|
||||
if [[ -e "$LOCK_FILE" ]]; then
|
||||
echo "Bailing, another compilation is running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
function lock {
|
||||
echo $$ >| "$LOCK_FILE"
|
||||
}
|
||||
function delete_lock_file {
|
||||
rm -f "$LOCK_FILE"
|
||||
}
|
||||
trap delete_lock_file SIGHUP SIGINT SIGTERM SIGEXIT
|
||||
lock
|
||||
|
||||
function main() {
|
||||
echo "* compile rss feed"
|
||||
compile_feeds
|
||||
# compile_feeds
|
||||
|
||||
echo "* harp compile $BLOG_DIR $TARGET"
|
||||
rm -rf "$TARGET/*" "$TARGET/.*"
|
||||
"$HARP" compile "$BLOG_DIR" "$TARGET"
|
||||
# echo "* harp compile $BLOG_DIR $TARGET"
|
||||
# rm -rf "$TARGET/*" "$TARGET/.*"
|
||||
# "$HARP" compile "$BLOG_DIR" "$TARGET"
|
||||
|
||||
# clean up temporary feeds
|
||||
rm $BLOG_DIR/public/feed.xml
|
||||
rm $BLOG_DIR/public/feed.json
|
||||
# rm $BLOG_DIR/public/feed.xml
|
||||
# rm $BLOG_DIR/public/feed.json
|
||||
|
||||
echo "* munge html files to make them available without an extension"
|
||||
munge_html
|
||||
# munge_html
|
||||
|
||||
echo "* inline CSS"
|
||||
ruby -w $DIR/inline-css.rb "$TARGET"
|
||||
# ruby -w $DIR/inline-css.rb "$TARGET"
|
||||
|
||||
echo "* minify js"
|
||||
minify_js
|
||||
|
||||
delete_lock_file
|
||||
# minify_js
|
||||
}
|
||||
|
||||
function compile_feeds() {
|
||||
|
|
@ -72,10 +55,10 @@ function munge_html() {
|
|||
done
|
||||
}
|
||||
|
||||
function minify_js() {
|
||||
for FILE in "$TARGET"/js/*.js; do
|
||||
$DIR/minify-js.sh "$FILE" > /tmp/minified.js && mv /tmp/minified.js "$FILE" || echo "* failed to minify $FILE"
|
||||
done
|
||||
}
|
||||
# function minify_js() {
|
||||
# for FILE in "$TARGET"/js/*.js; do
|
||||
# $DIR/minify-js.sh "$FILE" > /tmp/minified.js && mv /tmp/minified.js "$FILE" || echo "* failed to minify $FILE"
|
||||
# done
|
||||
# }
|
||||
|
||||
main
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
#!/bin/zsh
|
||||
|
||||
set -e # bail on errors
|
||||
|
||||
export PATH="$HOME/.rbenv/shims:$PATH"
|
||||
|
||||
BLOG_PATH="$1"
|
||||
ORIGIN_BLOG_PATH="${BLOG_PATH}-origin.git"
|
||||
if [[ -e "$BLOG_PATH" ]]; then
|
||||
echo ">>> Refusing to clobber $BLOG_PATH"
|
||||
else
|
||||
if [[ ! -e "$ORIGIN_BLOG_PATH" ]]; then
|
||||
echo ">>> Mirroring local origin..."
|
||||
git clone --mirror . "$ORIGIN_BLOG_PATH"
|
||||
fi
|
||||
echo ">>> Cloning test blog from local origin..."
|
||||
git clone "$ORIGIN_BLOG_PATH" "$BLOG_PATH"
|
||||
fi
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
DIR=$(dirname "$0")
|
||||
UGLIFY="node_modules/uglify-js/bin/uglifyjs"
|
||||
|
||||
function minify() {
|
||||
INPUT="$1"
|
||||
"$UGLIFY" "$INPUT"
|
||||
}
|
||||
|
||||
if [[ "$1" != "" ]]; then
|
||||
minify "$1"
|
||||
else
|
||||
echo "usage: $0 [input file]"
|
||||
exit 1
|
||||
fi
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"globals": {
|
||||
"site": "samhuri.net",
|
||||
"author": "Sami J. Samhuri",
|
||||
"email": "sami@samhuri.net",
|
||||
"url": "https://samhuri.net"
|
||||
}
|
||||
}
|
||||
39
nginx.conf
39
nginx.conf
|
|
@ -1,39 +0,0 @@
|
|||
server {
|
||||
listen 443;
|
||||
#listen [::]:443 default_server ipv6only=on; ## listen for ipv6
|
||||
server_name ocean.samhuri.net;
|
||||
|
||||
ssl on;
|
||||
ssl_certificate cert.pem;
|
||||
ssl_certificate_key cert.key;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:6706;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
server {
|
||||
listen 5000;
|
||||
#listen [::]:443 default_server ipv6only=on; ## listen for ipv6
|
||||
server_name ocean.samhuri.net;
|
||||
|
||||
ssl on;
|
||||
ssl_certificate cert.pem;
|
||||
ssl_certificate_key cert.key;
|
||||
ssl_session_timeout 5m;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
root /home/sjs/samhuri.net/www;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ $uri/index.html;
|
||||
}
|
||||
}
|
||||
8073
package-lock.json
generated
8073
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"name": "samhuri.net",
|
||||
"description": "samhuri.net",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"harp": "^0.31.0",
|
||||
"thepusher": "^0.1.4",
|
||||
"uglify-js": "^3.4.9",
|
||||
"snyk": "^1.41.1"
|
||||
},
|
||||
"scripts": {
|
||||
"snyk-protect": "snyk protect",
|
||||
"prepare": "npm run snyk-protect"
|
||||
},
|
||||
"snyk": true
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
# A sample Guardfile
|
||||
# More info at https://github.com/guard/guard#readme
|
||||
|
||||
# Note: The cmd option is now required due to the increasing number of ways
|
||||
# rspec may be run, below are examples of the most common uses.
|
||||
# * bundler: 'bundle exec rspec'
|
||||
# * bundler binstubs: 'bin/rspec'
|
||||
# * spring: 'bin/rsspec' (This will use spring if running and you have
|
||||
# installed the spring binstubs per the docs)
|
||||
# * zeus: 'zeus rspec' (requires the server to be started separetly)
|
||||
# * 'just' rspec: 'rspec'
|
||||
guard :rspec, cmd: 'bundle exec rspec' do
|
||||
watch('auth.json') { 'spec/server_spec.rb' }
|
||||
watch(%r{^(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
||||
|
||||
watch('spec/helpers.rb') { 'spec' }
|
||||
watch(%r{^spec/.+_spec\.rb$})
|
||||
end
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
require 'json'
|
||||
|
||||
class Auth
|
||||
def initialize(filename)
|
||||
@credentials = JSON.parse(File.read(filename))
|
||||
end
|
||||
|
||||
def authenticated?(username, password)
|
||||
@credentials['username'] == username && @credentials['password'] == password
|
||||
end
|
||||
end
|
||||
|
|
@ -1,467 +0,0 @@
|
|||
require 'fileutils'
|
||||
require 'json'
|
||||
require './harp_blog/post'
|
||||
require './web_title_finder'
|
||||
require './web_version_finder'
|
||||
|
||||
class HarpBlog
|
||||
|
||||
class HarpBlogError < RuntimeError ; end
|
||||
class InvalidDataError < HarpBlogError ; end
|
||||
class PostExistsError < HarpBlogError ; end
|
||||
class PostAlreadyPublishedError < HarpBlogError ; end
|
||||
class PostNotPublishedError < HarpBlogError ; end
|
||||
|
||||
class PostSaveError < HarpBlogError
|
||||
attr_reader :original_error
|
||||
def initialize(message, original_error)
|
||||
super(message)
|
||||
@original_error = original_error
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(path, dry_run = true, title_finder = nil, version_finder = nil)
|
||||
@path = path
|
||||
@dry_run = dry_run
|
||||
@title_finder = title_finder || WebTitleFinder.new
|
||||
@version_finder = version_finder || WebVersionFinder.new
|
||||
@mutated = false
|
||||
end
|
||||
|
||||
def local_version
|
||||
git_sha
|
||||
end
|
||||
|
||||
def remote_version
|
||||
@version_finder.find_version
|
||||
end
|
||||
|
||||
def dirty?
|
||||
local_version != remote_version
|
||||
end
|
||||
|
||||
def status
|
||||
local = local_version
|
||||
remote = remote_version
|
||||
{
|
||||
'local-version' => local,
|
||||
'remote-version' => remote,
|
||||
'dirty' => local != remote,
|
||||
}
|
||||
end
|
||||
|
||||
def years
|
||||
update_if_needed
|
||||
Dir[post_path('20*')].map { |x| File.basename(x) }.sort
|
||||
end
|
||||
|
||||
def months
|
||||
update_if_needed
|
||||
years.map do |year|
|
||||
# hack: month dirs (and only month dirs) are always 2 characters in length
|
||||
Dir[post_path("#{year}/??")].map { |x| [year, File.basename(x)] }
|
||||
end.flatten(1).sort
|
||||
end
|
||||
|
||||
def posts_for_year(year)
|
||||
posts = []
|
||||
1.upto(12) do |n|
|
||||
month = n < 10 ? "0#{n}" : "#{n}"
|
||||
posts += posts_for_month(year, month)
|
||||
end
|
||||
posts
|
||||
end
|
||||
|
||||
def posts_for_month(year, month)
|
||||
read_posts(File.join(year, month))
|
||||
end
|
||||
|
||||
def drafts
|
||||
read_posts('drafts', draft: true)
|
||||
end
|
||||
|
||||
def get_post(year, month, id)
|
||||
read_post(File.join(year, month), id)
|
||||
end
|
||||
|
||||
def get_draft(id)
|
||||
read_post('drafts', id, draft: true)
|
||||
end
|
||||
|
||||
def create_post(title, body, url, extra_fields = nil, options = nil)
|
||||
if !title || title.strip.length == 0
|
||||
title = url && find_title(url) or 'Untitled'
|
||||
end
|
||||
extra_fields ||= {}
|
||||
options ||= {}
|
||||
options[:commit] = true unless options.has_key?(:commit)
|
||||
fields = extra_fields.merge({
|
||||
title: title,
|
||||
link: url,
|
||||
body: body,
|
||||
})
|
||||
post = Post.new(fields)
|
||||
|
||||
begin
|
||||
existing_post = read_post(post.dir, post.id, extra_fields)
|
||||
rescue InvalidDataError => e
|
||||
$stderr.puts "[HarpBlog#create_post] deleting post with invalid data: #{e.message}"
|
||||
delete_post_from_dir(post.dir, post.id)
|
||||
existing_post = nil
|
||||
end
|
||||
|
||||
if existing_post
|
||||
raise PostExistsError.new("post exists: #{post.dir}/#{post.id}")
|
||||
else
|
||||
save_post(post)
|
||||
if options[:commit]
|
||||
git_commit("create post '#{post.title}'", [year_index_path("#{post.time.year}"), post_path(post.dir)])
|
||||
end
|
||||
post
|
||||
end
|
||||
end
|
||||
|
||||
def update_post(post, title, body, link, timestamp = nil)
|
||||
post.title = title
|
||||
post.body = body
|
||||
post.link = link
|
||||
post.timestamp = timestamp if timestamp
|
||||
save_post(post)
|
||||
git_commit("update post '#{post.title}'", [post_path(post.dir)])
|
||||
post
|
||||
end
|
||||
|
||||
def delete_post(year, month, id)
|
||||
dir = File.join(year, month)
|
||||
delete_post_from_dir(dir, id)
|
||||
git_commit("delete post #{year}/#{month}/#{id}", [post_path(dir)])
|
||||
end
|
||||
|
||||
def delete_draft(id, options = nil)
|
||||
options ||= {}
|
||||
options[:commit] = true unless options.has_key?(:commit)
|
||||
delete_post_from_dir('drafts', id)
|
||||
if options[:commit]
|
||||
git_commit("delete draft #{id}", [post_path('drafts')])
|
||||
end
|
||||
end
|
||||
|
||||
def publish_post(post)
|
||||
if post.draft?
|
||||
new_post = create_post(post.title, post.body, post.link, {}, {commit: false})
|
||||
delete_post_from_dir('drafts', post.id)
|
||||
build_rss
|
||||
git_commit("publish '#{quote(post.title)}'", [post_path('drafts'), post_path(new_post.dir), root_data_path])
|
||||
new_post
|
||||
else
|
||||
raise PostAlreadyPublishedError.new("post is already published: #{post.dir}/#{post.id}")
|
||||
end
|
||||
end
|
||||
|
||||
def unpublish_post(post)
|
||||
if post.draft?
|
||||
raise PostNotPublishedError.new("post is not published: #{post.dir}/#{post.id}")
|
||||
else
|
||||
new_post = create_post(post.title, post.body, post.link, {draft: true}, {commit: false})
|
||||
delete_post_from_dir(post.dir, post.id)
|
||||
build_rss
|
||||
git_commit("unpublish '#{quote(post.title)}'", [post_path(post.dir), post_path('drafts'), root_data_path])
|
||||
new_post
|
||||
end
|
||||
end
|
||||
|
||||
def publish(env)
|
||||
target = env.to_s == 'production' ? 'publish' : 'publish_beta'
|
||||
success, output = run("make #{target}")
|
||||
commit_root_data
|
||||
[success, output]
|
||||
end
|
||||
|
||||
def sync
|
||||
update_if_needed
|
||||
git_push
|
||||
end
|
||||
|
||||
def compile
|
||||
success, output = run('make compile')
|
||||
if success
|
||||
@mutated = false
|
||||
else
|
||||
puts output
|
||||
end
|
||||
success
|
||||
end
|
||||
|
||||
def compile_if_mutated
|
||||
compile if @mutated
|
||||
end
|
||||
|
||||
|
||||
############################################################################################
|
||||
private ##################################################################################
|
||||
############################################################################################
|
||||
|
||||
def find_title(url)
|
||||
@title_finder.find_title(url)
|
||||
end
|
||||
|
||||
def path_for(*components)
|
||||
File.join(@path, *components)
|
||||
end
|
||||
|
||||
def root_data_path
|
||||
path_for('public/_data.json')
|
||||
end
|
||||
|
||||
def year_index_path(year)
|
||||
path_for('public/posts', year, 'index.ejs')
|
||||
end
|
||||
|
||||
def post_path(dir, id = nil)
|
||||
args = ['public/posts', dir]
|
||||
args << "#{id}.md" if id
|
||||
path_for(*args)
|
||||
end
|
||||
|
||||
def read_posts(post_dir, extra_fields = nil)
|
||||
update_if_needed
|
||||
extra_fields ||= {}
|
||||
post_data = read_post_data(post_path(post_dir))
|
||||
post_data.sort_by do |k, v|
|
||||
(v['timestamp'] || Time.now).to_i
|
||||
end.map do |id, fields|
|
||||
fields[:id] = id
|
||||
unless extra_fields[:draft]
|
||||
fields[:slug] = id
|
||||
end
|
||||
post_filename = post_path(post_dir, id)
|
||||
fields[:body] = File.read(post_filename)
|
||||
Post.new(fields.merge(extra_fields))
|
||||
end
|
||||
end
|
||||
|
||||
def read_post(post_dir, id, extra_fields = nil)
|
||||
update_if_needed
|
||||
post_filename = post_path(post_dir, id)
|
||||
post_data = read_post_data(post_path(post_dir))
|
||||
if File.exist?(post_filename) && fields = post_data[id]
|
||||
fields[:body] = File.read(post_filename)
|
||||
if extra_fields
|
||||
fields.merge!(extra_fields)
|
||||
end
|
||||
fields[:id] = id
|
||||
unless fields[:draft]
|
||||
fields[:slug] = id
|
||||
end
|
||||
Post.new(fields)
|
||||
elsif fields
|
||||
message = "missing post body for #{post_dir}/#{id}: #{post_filename}"
|
||||
$stderr.puts "[HarpBlog#read_post] #{message}"
|
||||
raise InvalidDataError.new(message)
|
||||
elsif File.exist?(post_filename)
|
||||
message = "missing metadata for #{post_dir}/#{id}: #{post_dir}/_data.json"
|
||||
$stderr.puts "[HarpBlog#read_post] #{message}"
|
||||
raise InvalidDataError.new(message)
|
||||
end
|
||||
end
|
||||
|
||||
def save_post(post)
|
||||
update_if_needed
|
||||
begin
|
||||
write_post(post)
|
||||
post
|
||||
|
||||
rescue => e
|
||||
$stderr.puts "#{e.class}: #{e.message}"
|
||||
$stderr.puts e.backtrace
|
||||
git_reset_hard
|
||||
raise PostSaveError.new('failed to save post', e)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_post_from_dir(post_dir, id)
|
||||
update_if_needed
|
||||
post_dir = post_path(post_dir)
|
||||
delete_post_body(post_dir, id)
|
||||
delete_post_index(post_dir, id)
|
||||
end
|
||||
|
||||
def write_post(post)
|
||||
post_dir = post_path(post.dir)
|
||||
unless post.draft?
|
||||
ensure_post_dir_exists(post_dir)
|
||||
end
|
||||
write_post_body(post_dir, post.id, post.body)
|
||||
begin
|
||||
write_post_index(post_dir, post.id, post.persistent_fields)
|
||||
rescue => e
|
||||
$stderr.puts "#{e.class}: #{e.message}"
|
||||
$stderr.puts e.backtrace
|
||||
delete_post_body(post_dir, post.id)
|
||||
raise e
|
||||
end
|
||||
end
|
||||
|
||||
def write_post_body(dir, id, body)
|
||||
post_filename = File.join(dir, "#{id}.md")
|
||||
write_file(post_filename, body)
|
||||
end
|
||||
|
||||
def delete_post_body(dir, id)
|
||||
post_filename = File.join(dir, "#{id}.md")
|
||||
delete_file(post_filename)
|
||||
end
|
||||
|
||||
def write_post_index(dir, id, fields)
|
||||
post_data = read_post_data(dir)
|
||||
post_data[id] = fields
|
||||
write_post_data(dir, post_data)
|
||||
end
|
||||
|
||||
def delete_post_index(dir, id)
|
||||
post_data = read_post_data(dir)
|
||||
if post_data[id]
|
||||
post_data.delete(id)
|
||||
write_post_data(dir, post_data)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_post_dir_exists(dir)
|
||||
monthly_index_filename = File.join(dir, 'index.ejs')
|
||||
unless File.exist?(monthly_index_filename)
|
||||
source = File.join(dir, '../../2006/02/index.ejs')
|
||||
cp(source, monthly_index_filename)
|
||||
end
|
||||
|
||||
yearly_index_filename = File.join(dir, '../index.ejs')
|
||||
unless File.exist?(yearly_index_filename)
|
||||
source = File.join(dir, '../../2006/index.ejs')
|
||||
cp(source, yearly_index_filename)
|
||||
end
|
||||
end
|
||||
|
||||
def commit_root_data
|
||||
git_commit('commit root _data.json', [root_data_path])
|
||||
end
|
||||
|
||||
def read_post_data(dir)
|
||||
post_data_filename = File.join(dir, '_data.json')
|
||||
if File.exist?(post_data_filename)
|
||||
JSON.parse(File.read(post_data_filename))
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
|
||||
def write_post_data(dir, data)
|
||||
post_data_filename = File.join(dir, '_data.json')
|
||||
json = JSON.pretty_generate(data)
|
||||
write_file(post_data_filename, json)
|
||||
end
|
||||
|
||||
def ensure_dir_exists(dir)
|
||||
unless File.directory?(dir)
|
||||
if @dry_run
|
||||
puts ">>> mkdir -p '#{dir}'"
|
||||
else
|
||||
FileUtils.mkdir_p(dir)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cp(source, destination, clobber = false)
|
||||
ensure_dir_exists(File.dirname(destination))
|
||||
if !File.exist?(destination) || clobber
|
||||
if @dry_run
|
||||
puts ">>> cp '#{source}' '#{destination}'"
|
||||
else
|
||||
FileUtils.cp(source, destination)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def write_file(filename, data)
|
||||
ensure_dir_exists(File.dirname(filename))
|
||||
if @dry_run
|
||||
puts ">>> write file '#{filename}', contents:"
|
||||
puts data
|
||||
else
|
||||
File.write(filename, data)
|
||||
end
|
||||
end
|
||||
|
||||
def delete_file(filename)
|
||||
if File.exist?(filename)
|
||||
if @dry_run
|
||||
puts ">>> unlink '#{filename}'"
|
||||
else
|
||||
File.unlink(filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def quote(s)
|
||||
s.gsub('"', '\\"')
|
||||
end
|
||||
|
||||
def run(cmd, safety = :destructive)
|
||||
if safety == :destructive && @dry_run
|
||||
puts ">>> cd '#{@path}' && #{cmd}"
|
||||
[true, '']
|
||||
else
|
||||
output = `cd '#{@path}' && #{cmd} 2>&1`
|
||||
[$?.success?, output]
|
||||
end
|
||||
end
|
||||
|
||||
def git_sha
|
||||
update_if_needed
|
||||
_, output = run 'git log -n1 | head -n1 | cut -d" " -f2', :nondestructive
|
||||
output.strip
|
||||
end
|
||||
|
||||
def git_commit(message, files)
|
||||
quoted_files = files.map { |f| "\"#{quote(f)}\"" }
|
||||
puts ">>> git add -A #{quoted_files.join(' ')} && git commit -m \"#{message}\""
|
||||
success, output = run("git add -A #{quoted_files.join(' ')} && git commit -m \"#{message}\"")
|
||||
if success
|
||||
@mutated = true
|
||||
else
|
||||
puts output
|
||||
end
|
||||
[success, output]
|
||||
end
|
||||
|
||||
def git_reset_hard(ref = nil)
|
||||
args = ref ? "'#{ref}'" : ''
|
||||
run("git reset --hard #{args}")
|
||||
end
|
||||
|
||||
def git_push force = false
|
||||
args = force ? '-f' : nil
|
||||
run "git push #{args}"
|
||||
end
|
||||
|
||||
def origin_updated_path
|
||||
File.join @path, 'origin-updated'
|
||||
end
|
||||
|
||||
def git_update(remote = 'origin')
|
||||
if run "git update #{remote}", :nondestructive
|
||||
File.unlink origin_updated_path if origin_updated?
|
||||
end
|
||||
end
|
||||
|
||||
def origin_updated?
|
||||
File.exist? origin_updated_path
|
||||
end
|
||||
|
||||
def update_if_needed
|
||||
git_update if origin_updated?
|
||||
end
|
||||
|
||||
def build_rss
|
||||
run 'make rss'
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
require 'securerandom'
|
||||
|
||||
class HarpBlog
|
||||
class Post
|
||||
|
||||
PERSISTENT_FIELDS = %w[id author title date timestamp link url tags].map(&:to_sym)
|
||||
TRANSIENT_FIELDS = %w[time slug body draft].map(&:to_sym)
|
||||
FIELDS = PERSISTENT_FIELDS + TRANSIENT_FIELDS
|
||||
FIELDS.each { |f| attr_accessor f }
|
||||
|
||||
def initialize(fields = nil)
|
||||
@timestamp = nil
|
||||
if fields
|
||||
FIELDS.each do |k|
|
||||
if v = fields[k.to_s] || fields[k.to_sym]
|
||||
instance_variable_set("@#{k}", v)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def persistent_fields
|
||||
PERSISTENT_FIELDS.inject({}) do |h, k|
|
||||
h[k] = send(k)
|
||||
h
|
||||
end
|
||||
end
|
||||
|
||||
def fields
|
||||
FIELDS.inject({}) do |h, k|
|
||||
h[k] = send(k)
|
||||
h
|
||||
end
|
||||
end
|
||||
|
||||
def link?
|
||||
!!link
|
||||
end
|
||||
|
||||
def draft
|
||||
@draft ||= false
|
||||
end
|
||||
alias_method :draft?, :draft
|
||||
|
||||
def author
|
||||
@author ||= 'Sami J. Samhuri'
|
||||
end
|
||||
|
||||
def time
|
||||
@time ||= @timestamp ? Time.at(@timestamp) : Time.now
|
||||
end
|
||||
|
||||
def time=(time)
|
||||
@timestamp = nil
|
||||
@date = nil
|
||||
@url = nil
|
||||
@time = time
|
||||
end
|
||||
|
||||
def timestamp
|
||||
@timestamp ||= time.to_i
|
||||
end
|
||||
|
||||
def timestamp=(timestamp)
|
||||
@time = nil
|
||||
@date = nil
|
||||
@url = nil
|
||||
@timestamp = timestamp
|
||||
end
|
||||
|
||||
def id
|
||||
@id ||=
|
||||
if draft?
|
||||
SecureRandom.uuid
|
||||
else
|
||||
slug
|
||||
end
|
||||
end
|
||||
|
||||
def url
|
||||
if draft?
|
||||
"/posts/drafts/#{id}"
|
||||
else
|
||||
"/posts/#{time.year}/#{padded_month}/#{slug}"
|
||||
end
|
||||
end
|
||||
|
||||
def slug
|
||||
# TODO: be intelligent about unicode ... \p{Word} might help. negated char class with it?
|
||||
if !draft? && title
|
||||
@slug ||= title.downcase.
|
||||
gsub(/'/, '').
|
||||
gsub(/[^[:alpha:]\d_]/, '-').
|
||||
gsub(/^-+|-+$/, '').
|
||||
gsub(/-+/, '-')
|
||||
end
|
||||
end
|
||||
|
||||
def date
|
||||
@date ||= time.strftime("#{ordinalize(time.day)} %B, %Y")
|
||||
end
|
||||
|
||||
def ordinalize(n)
|
||||
case
|
||||
when n % 10 == 1 && n != 11
|
||||
"#{n}st"
|
||||
when n % 10 == 2 && n != 12
|
||||
"#{n}nd"
|
||||
when n % 10 == 3 && n != 13
|
||||
"#{n}rd"
|
||||
else
|
||||
"#{n}th"
|
||||
end
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= []
|
||||
end
|
||||
|
||||
def padded_month
|
||||
pad(time.month)
|
||||
end
|
||||
|
||||
def dir
|
||||
if draft?
|
||||
'drafts'
|
||||
else
|
||||
File.join(time.year.to_s, padded_month)
|
||||
end
|
||||
end
|
||||
|
||||
def pad(n)
|
||||
n.to_i < 10 ? "0#{n}" : "#{n}"
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
409
server/server.rb
409
server/server.rb
|
|
@ -1,409 +0,0 @@
|
|||
#!/usr/bin/env ruby -w
|
||||
|
||||
# An HTTP interface for my Harp blog.
|
||||
|
||||
require 'json'
|
||||
require 'sinatra'
|
||||
require './auth'
|
||||
require './harp_blog'
|
||||
|
||||
CONFIG_DEFAULTS = {
|
||||
auth: false,
|
||||
dry_run: false,
|
||||
path: File.expand_path('../test-blog', __FILE__),
|
||||
host: '127.0.0.1',
|
||||
hostname: `hostname --fqdn`.strip,
|
||||
port: 6706,
|
||||
external_port: 6706,
|
||||
external_ssl: false,
|
||||
preview_port: 9000,
|
||||
preview_ssl: false,
|
||||
}
|
||||
|
||||
def env_value(name)
|
||||
env_name = "BLOG_#{name.to_s.upcase}"
|
||||
raw_value = ENV[env_name]
|
||||
case name
|
||||
when :auth, :dry_run, :preview_ssl, :external_ssl
|
||||
raw_value ? raw_value.to_i != 0 : false
|
||||
when :port, :external_port, :preview_port
|
||||
raw_value ? raw_value.to_i : nil
|
||||
else
|
||||
raw_value
|
||||
end
|
||||
end
|
||||
|
||||
$config = CONFIG_DEFAULTS.dup
|
||||
$config.each_key do |name|
|
||||
value = env_value(name)
|
||||
unless value.nil?
|
||||
$config[name] = value
|
||||
end
|
||||
end
|
||||
|
||||
if $config[:external_ssl] && $config[:external_port] == 80
|
||||
$config[:external_port] = 443
|
||||
end
|
||||
if !$config[:external_ssl] && $config[:external_port] == 443
|
||||
$config[:external_ssl] = true
|
||||
end
|
||||
|
||||
proto = $config[:preview_ssl] ? 'https' : 'http'
|
||||
$config[:preview_url] = "#{proto}://#{$config[:hostname]}:#{$config[:preview_port]}"
|
||||
|
||||
unless File.directory?($config[:path])
|
||||
raise RuntimeError.new("file not found: #{$config[:path]}")
|
||||
end
|
||||
|
||||
if $config[:host] == '0.0.0.0' && !$config[:auth]
|
||||
raise RuntimeError.new("cowardly refusing to bind to 0.0.0.0 without authentication")
|
||||
end
|
||||
|
||||
$auth = Auth.new(File.expand_path('../auth.json', __FILE__))
|
||||
def authenticated?(auth)
|
||||
if $config[:auth]
|
||||
username, password = auth.to_s.split('|')
|
||||
$auth.authenticated?(username, password)
|
||||
else
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
host = $config[:hostname] || $config[:host]
|
||||
proto = $config[:external_ssl] ? 'https' : 'http'
|
||||
$url_root = "#{proto}://#{host}:#{$config[:external_port]}/"
|
||||
puts "URL root: #{$url_root}"
|
||||
def url_for(*components)
|
||||
File.join($url_root, *components)
|
||||
end
|
||||
|
||||
# Server
|
||||
|
||||
set :host, $config[:host]
|
||||
set :port, $config[:port]
|
||||
|
||||
blog = HarpBlog.new($config[:path], $config[:dry_run])
|
||||
|
||||
before do
|
||||
if request.body.size > 0
|
||||
@fields =
|
||||
case
|
||||
when request.accept?('application/json')
|
||||
request.body.rewind
|
||||
json = request.body.read
|
||||
JSON.parse(json)
|
||||
else
|
||||
params
|
||||
end
|
||||
else
|
||||
@fields = {}
|
||||
end
|
||||
end
|
||||
|
||||
# favicon
|
||||
get '/favicon.ico' do
|
||||
status 302
|
||||
headers 'Location' => "#{$config[:preview_url]}/favicon.ico"
|
||||
end
|
||||
|
||||
# status
|
||||
get '/status' do
|
||||
status 200
|
||||
headers 'Content-Type' => 'application/json'
|
||||
JSON.generate(status: blog.status)
|
||||
end
|
||||
|
||||
# publish the site
|
||||
post '/publish' do
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
blog.publish(@fields['env'])
|
||||
status 204
|
||||
end
|
||||
|
||||
# sync with github
|
||||
post '/sync' do
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
blog.sync
|
||||
status 204
|
||||
nil
|
||||
end
|
||||
|
||||
# list years
|
||||
get '/years' do
|
||||
status 200
|
||||
headers 'Content-Type' => 'application/json'
|
||||
JSON.generate(years: blog.years)
|
||||
end
|
||||
|
||||
# list months
|
||||
get '/months' do
|
||||
status 200
|
||||
headers 'Content-Type' => 'application/json'
|
||||
JSON.generate(months: blog.months)
|
||||
end
|
||||
|
||||
|
||||
##############
|
||||
### Drafts ###
|
||||
##############
|
||||
|
||||
# list drafts
|
||||
get '/posts/drafts' do
|
||||
posts = blog.drafts
|
||||
|
||||
status 200
|
||||
headers 'Content-Type' => 'application/json'
|
||||
JSON.generate(posts: posts.map(&:fields))
|
||||
end
|
||||
|
||||
# get a draft
|
||||
get '/posts/drafts/:id' do |id|
|
||||
begin
|
||||
post = blog.get_draft(id)
|
||||
rescue HarpBlog::InvalidDataError => e
|
||||
status 500
|
||||
return "Failed to get draft, invalid data on disk: #{e.message}"
|
||||
end
|
||||
|
||||
if post
|
||||
if request.accept?('text/html')
|
||||
status 302
|
||||
headers 'Location' => "#{$config[:preview_url]}/posts/drafts/#{id}"
|
||||
nil
|
||||
elsif request.accept?('application/json')
|
||||
status 200
|
||||
headers 'Content-Type' => 'application/json'
|
||||
JSON.generate(post: post.fields)
|
||||
else
|
||||
status 400
|
||||
"content not available in an acceptable format: #{request.accept.join(', ')}"
|
||||
end
|
||||
else
|
||||
status 404
|
||||
'not found'
|
||||
end
|
||||
end
|
||||
|
||||
# make a draft, and optionally publish it immediately
|
||||
post '/posts/drafts' do
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
id, title, body, link = @fields.values_at('id', 'title', 'body', 'link')
|
||||
begin
|
||||
if post = blog.create_post(title, body, link, id: id, draft: true)
|
||||
if env = @fields['env']
|
||||
post = blog.publish_post(post)
|
||||
Thread.new do
|
||||
blog.publish(env)
|
||||
end
|
||||
end
|
||||
url = url_for(post.url)
|
||||
status 201
|
||||
headers 'Location' => url, 'Content-Type' => 'application/json'
|
||||
JSON.generate(post: post.fields)
|
||||
else
|
||||
status 500
|
||||
'failed to create post'
|
||||
end
|
||||
rescue HarpBlog::PostExistsError => e
|
||||
post = HarpBlog::Post.new({
|
||||
title: title,
|
||||
body: body,
|
||||
link: link,
|
||||
})
|
||||
status 409
|
||||
"refusing to clobber existing draft, update it instead: #{post.url}"
|
||||
rescue HarpBlog::PostSaveError => e
|
||||
status 500
|
||||
if orig_err = e.original_error
|
||||
"#{e.message} -- #{orig_err.class}: #{orig_err.message}"
|
||||
else
|
||||
"Failed to create draft: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# update a draft
|
||||
put '/posts/drafts/:id' do |id|
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
title, body, link = @fields.values_at('title', 'body', 'link')
|
||||
begin
|
||||
if post = blog.get_draft(id)
|
||||
blog.update_post(post, title, body, link, Time.now.to_i)
|
||||
status 204
|
||||
else
|
||||
status 404
|
||||
'not found'
|
||||
end
|
||||
rescue HarpBlog::InvalidDataError => e
|
||||
status 500
|
||||
"Failed to update draft, invalid data on disk: #{e.message}"
|
||||
rescue HarpBlog::PostSaveError => e
|
||||
status 500
|
||||
if orig_err = e.original_error
|
||||
"#{e.message} -- #{orig_err.class}: #{orig_err.message}"
|
||||
else
|
||||
"Failed to update draft: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# delete a draft
|
||||
delete '/posts/drafts/:id' do |id|
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
blog.delete_draft(id)
|
||||
status 204
|
||||
end
|
||||
|
||||
# publish a post
|
||||
post '/posts/drafts/:id/publish' do |id|
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
if post = blog.get_draft(id)
|
||||
new_post = blog.publish_post(post)
|
||||
status 201
|
||||
headers 'Location' => url_for(new_post.url), 'Content-Type' => 'application/json'
|
||||
JSON.generate(post: new_post.fields)
|
||||
else
|
||||
status 404
|
||||
'not found'
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#######################
|
||||
### Published Posts ###
|
||||
#######################
|
||||
|
||||
# list published posts
|
||||
get '/posts/:year/?:month?' do |year, month|
|
||||
posts =
|
||||
if month
|
||||
blog.posts_for_month(year, month)
|
||||
else
|
||||
blog.posts_for_year(year)
|
||||
end
|
||||
|
||||
status 200
|
||||
headers 'Content-Type' => 'application/json'
|
||||
JSON.generate(posts: posts.map(&:fields))
|
||||
end
|
||||
|
||||
# list all published posts
|
||||
get '/posts' do
|
||||
posts = blog.months.map do |year, month|
|
||||
blog.posts_for_month(year, month)
|
||||
end.flatten.reverse
|
||||
|
||||
status 200
|
||||
headers 'Content-Type' => 'application/json'
|
||||
JSON.generate(posts: posts.map(&:fields))
|
||||
end
|
||||
|
||||
# get a post
|
||||
get '/posts/:year/:month/:id' do |year, month, id|
|
||||
begin
|
||||
post = blog.get_post(year, month, id)
|
||||
rescue HarpBlog::InvalidDataError => e
|
||||
status 500
|
||||
return "Failed to get post, invalid data on disk: #{e.message}"
|
||||
end
|
||||
|
||||
if post
|
||||
if request.accept?('text/html')
|
||||
status 302
|
||||
headers 'Location'=> "#{$config[:preview_url]}/posts/#{year}/#{month}/#{id}"
|
||||
nil
|
||||
elsif request.accept?('application/json')
|
||||
status 200
|
||||
headers 'Content-Type' => 'application/json'
|
||||
JSON.generate(post: post.fields)
|
||||
else
|
||||
status 400
|
||||
"content not available in an acceptable format: #{request.accept.join(', ')}"
|
||||
end
|
||||
else
|
||||
status 404
|
||||
'not found'
|
||||
end
|
||||
end
|
||||
|
||||
# update a post
|
||||
put '/posts/:year/:month/:id' do |year, month, id|
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
title, body, link = @fields.values_at('title', 'body', 'link')
|
||||
begin
|
||||
if post = blog.get_post(year, month, id)
|
||||
blog.update_post(post, title, body, link)
|
||||
status 204
|
||||
else
|
||||
status 404
|
||||
'not found'
|
||||
end
|
||||
rescue HarpBlog::InvalidDataError => e
|
||||
status 500
|
||||
"Failed to update post, invalid data on disk: #{e.message}"
|
||||
rescue HarpBlog::PostSaveError => e
|
||||
status 500
|
||||
if orig_err = e.original_error
|
||||
"#{e.message} -- #{orig_err.class}: #{orig_err.message}"
|
||||
else
|
||||
"Failed to update post: #{e.message}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# delete a post
|
||||
delete '/posts/:year/:month/:id' do |year, month, id|
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
blog.delete_post(year, month, id)
|
||||
status 204
|
||||
end
|
||||
|
||||
# unpublish a post
|
||||
post '/posts/:year/:month/:id/unpublish' do |year, month, id|
|
||||
unless authenticated?(request.env['HTTP_AUTH'])
|
||||
status 403
|
||||
return 'forbidden'
|
||||
end
|
||||
|
||||
if post = blog.get_post(year, month, id)
|
||||
new_post = blog.unpublish_post(post)
|
||||
status 200
|
||||
headers 'Location' => url_for(new_post.url), 'Content-Type' => 'application/json'
|
||||
JSON.generate(post: new_post.fields)
|
||||
else
|
||||
status 404
|
||||
'not found'
|
||||
end
|
||||
end
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
require_relative '../harp_blog/post'
|
||||
|
||||
RSpec.describe HarpBlog::Post do
|
||||
|
||||
# Persistent fields: id, author, title, date, timestamp, link, url, tags
|
||||
# Transient fields: time, slug, body
|
||||
|
||||
before :all do
|
||||
@post_fields = {
|
||||
title: 'samhuri.net',
|
||||
link: 'https://samhuri.net',
|
||||
body: 'this site is sick',
|
||||
}
|
||||
@post_slug = 'samhuri-net'
|
||||
@draft_fields = {
|
||||
title: 'reddit.com',
|
||||
link: 'http://reddit.com',
|
||||
body: 'hi reddit',
|
||||
draft: true,
|
||||
id: 'dummy-draft-id',
|
||||
}
|
||||
end
|
||||
|
||||
describe '#new' do
|
||||
it "takes a Hash of fields" do
|
||||
fields = @post_fields
|
||||
post = HarpBlog::Post.new(fields)
|
||||
expect(post.title).to eq(fields[:title])
|
||||
expect(post.link).to eq(fields[:link])
|
||||
expect(post.body).to eq(fields[:body])
|
||||
end
|
||||
|
||||
it "accepts no parameters" do
|
||||
post = HarpBlog::Post.new
|
||||
expect(post).to be_truthy
|
||||
end
|
||||
|
||||
it "ignores unknown fields" do
|
||||
post = HarpBlog::Post.new(what: 'is this')
|
||||
expect(post).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#persistent_fields' do
|
||||
it "contains all expected fields" do
|
||||
all_keys = HarpBlog::Post::PERSISTENT_FIELDS.sort
|
||||
post = HarpBlog::Post.new
|
||||
expect(all_keys).to eq(post.persistent_fields.keys.sort)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#fields' do
|
||||
it "contains all expected fields" do
|
||||
all_keys = HarpBlog::Post::FIELDS.sort
|
||||
post = HarpBlog::Post.new
|
||||
expect(all_keys).to eq(post.fields.keys.sort)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#link?' do
|
||||
it "returns true for link posts" do
|
||||
post = HarpBlog::Post.new(link: @post_fields[:link])
|
||||
expect(post.link?).to be_truthy
|
||||
end
|
||||
|
||||
it "returns false for article posts" do
|
||||
post = HarpBlog::Post.new
|
||||
expect(post.link?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#draft?' do
|
||||
it "returns true for draft posts" do
|
||||
post = HarpBlog::Post.new(draft: true)
|
||||
expect(post.draft?).to be_truthy
|
||||
end
|
||||
|
||||
it "returns false for published posts" do
|
||||
post = HarpBlog::Post.new
|
||||
expect(post.draft?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#time' do
|
||||
it "should be derived from the timestamp if necessary" do
|
||||
timestamp = Time.now.to_i
|
||||
post = HarpBlog::Post.new(timestamp: timestamp)
|
||||
expect(post.time.to_i).to eq(timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#timestamp' do
|
||||
it "should be derived from the time if necessary" do
|
||||
time = Time.now - 42
|
||||
post = HarpBlog::Post.new(time: time)
|
||||
expect(post.timestamp).to eq(time.to_i)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#url' do
|
||||
it "should be derived from the time and slug if necessary" do
|
||||
post = HarpBlog::Post.new(@post_fields)
|
||||
year = post.time.year.to_s
|
||||
month = post.time.month
|
||||
padded_month = month < 10 ? "0#{month}" : "#{month}"
|
||||
expect(post.url).to eq("/posts/#{year}/#{padded_month}/#{@post_slug}")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#id' do
|
||||
it "should be generated for drafts if necessary" do
|
||||
draft = HarpBlog::Post.new(@draft_fields)
|
||||
expect(draft.id).to eq(@draft_fields[:id])
|
||||
|
||||
draft = HarpBlog::Post.new(@draft_fields.merge(id: nil))
|
||||
expect(draft.id).to_not eq(@draft_fields[:id])
|
||||
end
|
||||
|
||||
it "should be the slug for posts" do
|
||||
post = HarpBlog::Post.new(@post_fields)
|
||||
expect(post.id).to eq(post.slug)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#slug' do
|
||||
it "should be derived from the title if necessary" do
|
||||
post = HarpBlog::Post.new(@post_fields)
|
||||
expect(post.slug).to eq(@post_slug)
|
||||
end
|
||||
|
||||
it "should be nil for drafts" do
|
||||
draft = HarpBlog::Post.new(@draft_fields)
|
||||
expect(draft.slug).to be_nil
|
||||
end
|
||||
|
||||
it "should strip apostrophes" do
|
||||
post = HarpBlog::Post.new(title: "sjs's post")
|
||||
expect(post.slug).to eq('sjss-post')
|
||||
end
|
||||
|
||||
it "should replace most non-word characters with dashes" do
|
||||
post = HarpBlog::Post.new(title: 'foo/bår!baz_quüx42')
|
||||
expect(post.slug).to eq('foo-bår-baz_quüx42')
|
||||
end
|
||||
|
||||
it "should strip leading and trailing dashes" do
|
||||
post = HarpBlog::Post.new(title: '!foo?bar!')
|
||||
expect(post.slug).to eq('foo-bar')
|
||||
end
|
||||
|
||||
it "should collapse runs of dashes" do
|
||||
post = HarpBlog::Post.new(title: 'foo???bar')
|
||||
expect(post.slug).to eq('foo-bar')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#dir' do
|
||||
it "returns the drafts dir for draft posts" do
|
||||
post = HarpBlog::Post.new(draft: true)
|
||||
expect(post.dir).to eq('drafts')
|
||||
end
|
||||
|
||||
it "returns the dated dir for published posts" do
|
||||
post = HarpBlog::Post.new
|
||||
expect(post.dir).to eq("#{post.time.year}/#{post.padded_month}")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pad' do
|
||||
it "should have a leading zero for integers 0 < n < 10" do
|
||||
post = HarpBlog::Post.new
|
||||
expect(post.pad(1)).to eq('01')
|
||||
expect(post.pad(9)).to eq('09')
|
||||
end
|
||||
|
||||
it "should not have a leading zero for integers n >= 10" do
|
||||
post = HarpBlog::Post.new
|
||||
expect(post.pad(10)).to eq('10')
|
||||
expect(post.pad(12)).to eq('12')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,359 +0,0 @@
|
|||
require 'json'
|
||||
require_relative './helpers'
|
||||
require_relative '../harp_blog'
|
||||
|
||||
TEST_BLOG_PATH = File.expand_path('../../test-blog', __FILE__)
|
||||
TEST_BLOG_ORIGIN_PATH = File.expand_path('../../test-blog-origin.git', __FILE__)
|
||||
|
||||
RSpec.configure do |c|
|
||||
c.include Helpers
|
||||
end
|
||||
|
||||
RSpec.describe HarpBlog do
|
||||
before :each do
|
||||
@test_blog_ref = git_sha(TEST_BLOG_PATH)
|
||||
dry_run = false
|
||||
@mock_title = 'fancy title'
|
||||
@mock_title_finder = mock_title_finder(@mock_title)
|
||||
@mock_version_finder = mock_version_finder(@test_blog_ref)
|
||||
@blog = HarpBlog.new(TEST_BLOG_PATH, dry_run, @mock_title_finder, @mock_version_finder)
|
||||
end
|
||||
|
||||
after :each do
|
||||
git_reset_hard(TEST_BLOG_PATH, @test_blog_ref)
|
||||
force = true
|
||||
@blog.send(:git_push, force)
|
||||
end
|
||||
|
||||
describe '#new' do
|
||||
it "should optionally accept dry_run" do
|
||||
expect(@blog).to be_truthy
|
||||
|
||||
blog = HarpBlog.new(TEST_BLOG_PATH)
|
||||
expect(blog).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#local_version' do
|
||||
it "should expose the local version" do
|
||||
expect(@blog.local_version).to eq(@test_blog_ref)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#remote_version' do
|
||||
it "should expose the remote version" do
|
||||
expect(@blog.remote_version).to eq(@test_blog_ref)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#dirty?' do
|
||||
it "should specify whether or not there are unpublished changes" do
|
||||
expect(@blog.dirty?).to be_falsy
|
||||
|
||||
@blog.create_post('title', 'body', nil)
|
||||
expect(@blog.dirty?).to be_truthy
|
||||
|
||||
@mock_version_finder.version = @blog.local_version
|
||||
expect(@blog.dirty?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#status' do
|
||||
it "should expose the local and remote versions, and dirty state" do
|
||||
status = @blog.status
|
||||
expect(status['local-version']).to eq(@blog.local_version)
|
||||
expect(status['remote-version']).to eq(@blog.remote_version)
|
||||
expect(status['dirty']).to eq(@blog.dirty?)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#years' do
|
||||
it "should return all of the years with posts" do
|
||||
# yup, if I don't blog for an entire year that's a bug!
|
||||
years = (2006..Date.today.year).to_a.map(&:to_s)
|
||||
expect(@blog.years).to eq(years)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#months' do
|
||||
it "should return all of the years and months with posts" do
|
||||
months = [
|
||||
["2006", "02"], ["2006", "03"], ["2006", "04"], ["2006", "05"], ["2006", "06"], ["2006", "07"], ["2006", "08"], ["2006", "09"], ["2006", "12"],
|
||||
["2007", "03"], ["2007", "04"], ["2007", "05"], ["2007", "06"], ["2007", "07"], ["2007", "08"], ["2007", "09"], ["2007", "10"],
|
||||
["2008", "01"], ["2008", "02"], ["2008", "03"],
|
||||
["2009", "11"],
|
||||
["2010", "01"], ["2010", "11"],
|
||||
["2011", "11"], ["2011", "12"],
|
||||
["2012", "01"],
|
||||
["2013", "03"], ["2013", "09"],
|
||||
]
|
||||
expect(@blog.months.first(months.length)).to eq(months)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#posts_for_month' do
|
||||
it "should return the correct number of posts" do
|
||||
expect(@blog.posts_for_month('2006', '02').length).to eq(12)
|
||||
end
|
||||
|
||||
it "should sort the posts by publish time" do
|
||||
timestamps = @blog.posts_for_month('2006', '02').map(&:timestamp)
|
||||
expect(increasing?(timestamps)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#posts_for_year' do
|
||||
it "should return the correct number of posts" do
|
||||
expect(@blog.posts_for_year('2006').length).to eq(31)
|
||||
end
|
||||
|
||||
it "should sort the posts by publish time" do
|
||||
timestamps = @blog.posts_for_year('2006').map(&:timestamp)
|
||||
expect(increasing?(timestamps)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#drafts' do
|
||||
it "returns the correct number of posts" do
|
||||
expect(@blog.drafts.length).to eq(2)
|
||||
end
|
||||
|
||||
it "should sort the posts by publish time" do
|
||||
timestamps = @blog.drafts.map(&:timestamp)
|
||||
expect(increasing?(timestamps)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_post' do
|
||||
it "should return complete posts" do
|
||||
first_post_path = File.join(TEST_BLOG_PATH, 'public/posts/2006/02/first-post.md')
|
||||
post = @blog.get_post('2006', '02', 'first-post')
|
||||
expect(post).to be_truthy
|
||||
expect(post.author).to eq('Sami J. Samhuri')
|
||||
expect(post.title).to eq('First Post!')
|
||||
expect(post.slug).to eq('first-post')
|
||||
expect(post.timestamp).to eq(1139368860)
|
||||
expect(post.date).to eq('8th February, 2006')
|
||||
expect(post.url).to eq('/posts/2006/02/first-post')
|
||||
expect(post.link).to eq(nil)
|
||||
expect(post.link?).to be_falsy
|
||||
expect(post.tags).to eq(['life'])
|
||||
expect(post.body).to eq(File.read(first_post_path))
|
||||
end
|
||||
|
||||
it "should return nil if the post does not exist" do
|
||||
post = @blog.get_post('2005', '01', 'anything')
|
||||
expect(post).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_draft' do
|
||||
it "should return complete posts" do
|
||||
id = 'some-draft-id'
|
||||
title = 'new draft'
|
||||
body = "blah blah blah\n"
|
||||
@blog.create_post(title, body, nil, id: id, draft: true)
|
||||
draft = @blog.get_draft(id)
|
||||
expect(draft).to be_truthy
|
||||
expect(draft.title).to eq(title)
|
||||
expect(draft.url).to eq("/posts/drafts/#{id}")
|
||||
expect(draft.draft?).to be_truthy
|
||||
expect(draft.body).to eq(body)
|
||||
end
|
||||
|
||||
it "should return nil if the post does not exist" do
|
||||
draft = @blog.get_draft('does-not-exist')
|
||||
expect(draft).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#create_post' do
|
||||
it "should create a link post when a link is given" do
|
||||
title = 'test post'
|
||||
body = 'check this out'
|
||||
link = 'https://samhuri.net'
|
||||
post = @blog.create_post(title, body, link)
|
||||
expect(post).to be_truthy
|
||||
expect(post.link?).to be_truthy
|
||||
expect(post.title).to eq(title)
|
||||
expect(post.body).to eq(body)
|
||||
expect(post.link).to eq(link)
|
||||
expect(post.time.to_date).to eq(Date.today)
|
||||
end
|
||||
|
||||
it "should create an article post when no link is given" do
|
||||
title = 'test post'
|
||||
body = 'check this out'
|
||||
post = @blog.create_post(title, body, nil)
|
||||
expect(post).to be_truthy
|
||||
expect(post.link?).to be_falsy
|
||||
expect(post.title).to eq(title)
|
||||
expect(post.body).to eq(body)
|
||||
expect(post.link).to eq(nil)
|
||||
expect(post.time.to_date).to eq(Date.today)
|
||||
end
|
||||
|
||||
it "should create a draft post" do
|
||||
title = 'test draft'
|
||||
body = 'check this out'
|
||||
post = @blog.create_post(title, body, nil, draft: true)
|
||||
expect(post).to be_truthy
|
||||
expect(post.draft?).to be_truthy
|
||||
expect(post.dir).to eq('drafts')
|
||||
end
|
||||
|
||||
it "should create a post that can be fetched immediately" do
|
||||
title = 'fetch now'
|
||||
body = 'blah blah blah'
|
||||
post = @blog.create_post(title, body, nil)
|
||||
expect(post).to be_truthy
|
||||
|
||||
today = Date.today
|
||||
year = today.year.to_s
|
||||
month = post.pad(today.month)
|
||||
fetched_post = @blog.get_post(year, month, post.slug)
|
||||
expect(fetched_post.url).to eq(post.url)
|
||||
end
|
||||
|
||||
it "should create a draft that can be fetched immediately" do
|
||||
id = 'another-draft-id'
|
||||
title = 'fetch now'
|
||||
body = 'blah blah blah'
|
||||
draft = @blog.create_post(title, body, nil, id: id, draft: true)
|
||||
expect(draft).to be_truthy
|
||||
|
||||
fetched_draft = @blog.get_draft(draft.id)
|
||||
expect(draft.url).to eq(fetched_draft.url)
|
||||
end
|
||||
|
||||
it "should fetch titles if necessary" do
|
||||
post = @blog.create_post(nil, nil, 'https://samhuri.net')
|
||||
expect(post.title).to eq(@mock_title)
|
||||
@blog.delete_post(post.time.year.to_s, post.padded_month, post.slug)
|
||||
post = @blog.create_post(" \t\n", nil, 'https://samhuri.net')
|
||||
expect(post.title).to eq(@mock_title)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_post' do
|
||||
it "should immediately reflect changes when fetched" do
|
||||
post = @blog.get_post('2006', '02', 'first-post')
|
||||
title = 'new title'
|
||||
body = "new body\n"
|
||||
link = 'new link'
|
||||
@blog.update_post(post, title, body, link)
|
||||
|
||||
# new slug, new data
|
||||
post = @blog.get_post('2006', '02', 'first-post')
|
||||
expect(post.title).to eq(title)
|
||||
expect(post.body).to eq(body)
|
||||
expect(post.link).to eq(link)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_post' do
|
||||
it "should delete existing posts" do
|
||||
post = @blog.get_post('2006', '02', 'first-post')
|
||||
expect(post).to be_truthy
|
||||
|
||||
@blog.delete_post('2006', '02', 'first-post')
|
||||
|
||||
post = @blog.get_post('2006', '02', 'first-post')
|
||||
expect(post).to eq(nil)
|
||||
end
|
||||
|
||||
it "should do nothing for non-existent posts" do
|
||||
post = @blog.get_post('2006', '02', 'first-post')
|
||||
expect(post).to be_truthy
|
||||
|
||||
@blog.delete_post('2006', '02', 'first-post')
|
||||
@blog.delete_post('2006', '02', 'first-post')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#delete_draft' do
|
||||
it "should delete existing drafts" do
|
||||
id = 'bunk-draft-id'
|
||||
title = 'new draft'
|
||||
body = 'blah blah blah'
|
||||
existing_draft = @blog.create_post(title, body, nil, id: id, draft: true)
|
||||
draft = @blog.get_draft(existing_draft.id)
|
||||
expect(draft).to be_truthy
|
||||
|
||||
@blog.delete_draft(draft.id)
|
||||
|
||||
draft = @blog.get_draft(draft.id)
|
||||
expect(draft).to eq(nil)
|
||||
end
|
||||
|
||||
it "should do nothing for non-existent posts" do
|
||||
id = 'missing-draft-id'
|
||||
title = 'new draft'
|
||||
body = 'blah blah blah'
|
||||
existing_draft = @blog.create_post(title, body, nil, id: id, draft: true)
|
||||
|
||||
draft = @blog.get_draft(existing_draft.id)
|
||||
expect(draft).to be_truthy
|
||||
|
||||
@blog.delete_draft(draft.id)
|
||||
expect(@blog.get_draft(existing_draft.id)).to be_nil
|
||||
@blog.delete_draft(draft.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#publish_post' do
|
||||
it "should publish drafts" do
|
||||
id = 'this-draft-is-a-keeper'
|
||||
title = 'a-shiny-new-post'
|
||||
body = 'blah blah blah'
|
||||
link = 'https://samhuri.net'
|
||||
draft = @blog.create_post(title, body, link, id: id, draft: true)
|
||||
post = @blog.publish_post(draft)
|
||||
expect(post).to be_truthy
|
||||
expect(post.id).to eq(post.slug)
|
||||
expect(post.draft?).to be_falsy
|
||||
expect(post.title).to eq(title)
|
||||
expect(post.body).to eq(body)
|
||||
expect(post.link).to eq(link)
|
||||
|
||||
missing_draft = @blog.get_draft(draft.id)
|
||||
expect(missing_draft).to eq(nil)
|
||||
|
||||
fetched_post = @blog.get_post(post.time.year.to_s, post.padded_month, post.slug)
|
||||
expect(fetched_post).to be_truthy
|
||||
end
|
||||
|
||||
it "should raise an error for published posts" do
|
||||
post = @blog.get_post('2006', '02', 'first-post')
|
||||
expect { @blog.publish_post(post) }.to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '#unpublish_post' do
|
||||
it "should unpublish posts" do
|
||||
post = @blog.get_post('2006', '02', 'first-post')
|
||||
draft = @blog.unpublish_post(post)
|
||||
expect(draft).to be_truthy
|
||||
expect(draft.id).to be_truthy
|
||||
expect(draft.draft?).to be_truthy
|
||||
expect(draft.title).to eq(post.title)
|
||||
expect(draft.body).to eq(post.body)
|
||||
expect(draft.link).to eq(post.link)
|
||||
|
||||
missing_post = @blog.get_post(post.time.year.to_s, post.padded_month, post.slug)
|
||||
expect(missing_post).to eq(nil)
|
||||
|
||||
fetched_draft = @blog.get_draft(draft.id)
|
||||
expect(fetched_draft).to be_truthy
|
||||
end
|
||||
|
||||
it "should raise an error for drafts" do
|
||||
title = 'a-shiny-new-post'
|
||||
body = 'blah blah blah'
|
||||
link = 'https://samhuri.net'
|
||||
post = @blog.create_post(title, body, link, draft: true)
|
||||
expect { @blog.unpublish_post(post) }.to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
module Helpers
|
||||
|
||||
def increasing?(list)
|
||||
comparisons(list).all? { |x| x && x <= 0 }
|
||||
end
|
||||
|
||||
def decreasing?(list)
|
||||
comparisons(list).all? { |x| x && x >= 0 }
|
||||
end
|
||||
|
||||
def comparisons(list)
|
||||
x = list.first
|
||||
list.drop(1).map do |y|
|
||||
x <=> y
|
||||
end
|
||||
end
|
||||
|
||||
def git_bare?(dir)
|
||||
if File.exist?(File.join(dir, '.git'))
|
||||
false
|
||||
elsif File.exist?(File.join(dir, 'HEAD'))
|
||||
true
|
||||
else
|
||||
raise "what is this dir? #{dir} #{Dir[dir + '/*'].join(', ')}"
|
||||
end
|
||||
end
|
||||
|
||||
def git_sha(dir)
|
||||
if git_bare?(dir)
|
||||
`cd '#{dir}' && cat "$(cut -d' ' -f2 HEAD)"`.strip
|
||||
else
|
||||
`cd '#{dir}' && git log -n1`.split[1].strip
|
||||
end
|
||||
end
|
||||
|
||||
def git_reset_hard(dir, ref = nil)
|
||||
if git_bare?(dir)
|
||||
raise 'git_reset_hard does not support bare repos ' + dir + ' -- ' + ref.to_s
|
||||
else
|
||||
args = ref ? "'#{ref}'" : ''
|
||||
`cd '#{dir}' && git reset --hard #{args}`
|
||||
end
|
||||
end
|
||||
|
||||
class TitleFinder
|
||||
attr_accessor :title
|
||||
def initialize(title)
|
||||
@title = title
|
||||
end
|
||||
def find_title(url) @title end
|
||||
end
|
||||
|
||||
def mock_title_finder(title)
|
||||
TitleFinder.new(title)
|
||||
end
|
||||
|
||||
class VersionFinder
|
||||
attr_accessor :version
|
||||
def initialize(version)
|
||||
@version = version
|
||||
end
|
||||
def find_version(url = nil)
|
||||
@version
|
||||
end
|
||||
end
|
||||
|
||||
def mock_version_finder(version)
|
||||
VersionFinder.new(version)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
require 'htmlentities'
|
||||
require 'open-uri'
|
||||
|
||||
class WebTitleFinder
|
||||
|
||||
def find_title(url)
|
||||
body = open(url).read
|
||||
lines = body.split(/[\r\n]+/)
|
||||
title_line = lines.grep(/<title/).first.strip
|
||||
html_title = title_line.gsub(/\s*<\/?title[^>]*>\s*/, '')
|
||||
HTMLEntities.new.decode(html_title)
|
||||
rescue
|
||||
nil
|
||||
end
|
||||
|
||||
end
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
require 'open-uri'
|
||||
|
||||
class WebVersionFinder
|
||||
|
||||
DEFAULT_URL = 'https://samhuri.net/version.txt'
|
||||
|
||||
def find_version(url = nil)
|
||||
open(url || DEFAULT_URL).read.strip
|
||||
end
|
||||
|
||||
end
|
||||
6
site.json
Normal file
6
site.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"title": "samhuri.net",
|
||||
"author": "Sami J. Samhuri",
|
||||
"email": "sami@samhuri.net",
|
||||
"url": "https://samhuri.net"
|
||||
}
|
||||
Loading…
Reference in a new issue