qiita-export 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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +51 -0
- data/Rakefile +11 -0
- data/bin/qiita-export +19 -0
- data/lib/qiita-export.rb +10 -0
- data/lib/qiita-export/article.rb +65 -0
- data/lib/qiita-export/config.rb +184 -0
- data/lib/qiita-export/exporter.rb +45 -0
- data/lib/qiita-export/fetcher/api_fetcher.rb +42 -0
- data/lib/qiita-export/fetcher/base.rb +22 -0
- data/lib/qiita-export/fetcher/kobito_fetcher.rb +78 -0
- data/lib/qiita-export/fetcher/url_fetcher.rb +24 -0
- data/lib/qiita-export/fetcher/user_fetcher.rb +35 -0
- data/lib/qiita-export/image.rb +49 -0
- data/lib/qiita-export/version.rb +4 -0
- data/qiita-export.gemspec +27 -0
- data/spec/qiita-export/config_spec.rb +47 -0
- data/spec/qiita-export/image_spec.rb +80 -0
- data/spec/spec_helper.rb +9 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6442dd02c9dd12b8a17344ad0a9e3f6dd780c371
|
4
|
+
data.tar.gz: 1463061332a477bf955edcbd435db8a3c0fb6279
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4668ef573aaa449d7d37248bc060a05700f555fd3bf16158d412b2809f20794130c1e5955d8cf15805966e2801ee861595a56deb79ae0970892007d4b76b943b
|
7
|
+
data.tar.gz: ec2cb0016242ce4a2ee41e0d8926facb234e4f77b06142a4e73371ff3f950f97f6f92995529f25c8618f1b342d14fdde3c6a315e899101b22b80e90e22ecacbd
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 akishin
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# QiitaExport
|
2
|
+
|
3
|
+
export tool for Qiita(http://qiita.com/).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'qiita_export'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
```
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
```
|
22
|
+
$ gem install qiita_export
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
```
|
28
|
+
Usage: qiita-export [options]
|
29
|
+
-u, --url=url specify the URL for the Qiita(or Qiita Team).
|
30
|
+
-l, --url-list=filepath specify the file path of the URL list.
|
31
|
+
-U, --user-id=user_id specify the userid for the Qiita.
|
32
|
+
-k, --kobito=[Kobito.db] export Kobito.app database.
|
33
|
+
-t, --team=teamname export Qiita Team articles only.
|
34
|
+
-i, --image export with images.
|
35
|
+
-h, --html export in html format(experimental).
|
36
|
+
-o, --output-dir=dirpath specify the full path of destination directory.
|
37
|
+
-a, --api-token=token specify API token for Qiita.
|
38
|
+
```
|
39
|
+
|
40
|
+
## Contributing
|
41
|
+
|
42
|
+
1. Fork it ( https://github.com/akishin/qiita_export/fork )
|
43
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
44
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
45
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
46
|
+
5. Create a new Pull Request
|
47
|
+
|
48
|
+
## License
|
49
|
+
|
50
|
+
MIT License
|
51
|
+
|
data/Rakefile
ADDED
data/bin/qiita-export
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
3
|
+
require 'rubygems'
|
4
|
+
require 'qiita-export'
|
5
|
+
|
6
|
+
QiitaExport::Config.configure(ARGV)
|
7
|
+
if QiitaExport::Config.empty?
|
8
|
+
print QiitaExport::Config.help
|
9
|
+
exit true
|
10
|
+
end
|
11
|
+
|
12
|
+
begin
|
13
|
+
QiitaExport::Config.validate!
|
14
|
+
rescue ArgumentError => e
|
15
|
+
$stderr.puts e.message
|
16
|
+
exit(1)
|
17
|
+
end
|
18
|
+
|
19
|
+
QiitaExport::Exporter::new.export
|
data/lib/qiita-export.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'qiita-export/version'
|
2
|
+
require 'qiita-export/image'
|
3
|
+
require 'qiita-export/article'
|
4
|
+
require 'qiita-export/config'
|
5
|
+
require 'qiita-export/exporter'
|
6
|
+
require 'qiita-export/fetcher/base'
|
7
|
+
require 'qiita-export/fetcher/kobito_fetcher'
|
8
|
+
require 'qiita-export/fetcher/api_fetcher'
|
9
|
+
require 'qiita-export/fetcher/url_fetcher'
|
10
|
+
require 'qiita-export/fetcher/user_fetcher'
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
module QiitaExport
|
5
|
+
class Article
|
6
|
+
attr_reader :key, :url, :title, :raw_body, :rendered_body, :created_at,
|
7
|
+
:updated_at, :user_id, :images
|
8
|
+
|
9
|
+
HTML_TEMPLATE =<<-"EOS"
|
10
|
+
<!DOCTYPE html>
|
11
|
+
<html>
|
12
|
+
<head>
|
13
|
+
<meta charset="UTF-8" />
|
14
|
+
<title><%= @title %></title>
|
15
|
+
</head>
|
16
|
+
<body>
|
17
|
+
<h1><%= @title %></h1>
|
18
|
+
<%= @rendered_body %>
|
19
|
+
</body>
|
20
|
+
</html>
|
21
|
+
EOS
|
22
|
+
|
23
|
+
def initialize(key, url, title, raw_body, rendered_body, created_at, updated_at, user_id)
|
24
|
+
@key = key
|
25
|
+
@url = url
|
26
|
+
@title = title
|
27
|
+
@raw_body = raw_body
|
28
|
+
@rendered_body = rendered_body
|
29
|
+
@created_at = created_at
|
30
|
+
@updated_at = updated_at
|
31
|
+
@user_id = user_id
|
32
|
+
@images = Image.extract_urls(@key, @raw_body)
|
33
|
+
end
|
34
|
+
|
35
|
+
def save
|
36
|
+
save_dir = File.join(Config.export_dir_path, Config.team_name(@url), @key)
|
37
|
+
|
38
|
+
FileUtils.makedirs(save_dir) unless Dir.exists?(save_dir)
|
39
|
+
|
40
|
+
file_path = File.join(save_dir, Config.filename(title))
|
41
|
+
File.open(file_path, "w") { |f| f.write export_content }
|
42
|
+
if (Config.image_export?)
|
43
|
+
@images.each { |image| image.save(save_dir) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def export_content
|
50
|
+
if Config.html_export?
|
51
|
+
replace_img_src!(@rendered_body)
|
52
|
+
ERB.new(HTML_TEMPLATE).result(binding)
|
53
|
+
else
|
54
|
+
@raw_body
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def replace_img_src!(html)
|
59
|
+
image_urls = html.scan(/<img[^src]+src="([^"]+)"[^>]+>/).flatten
|
60
|
+
image_urls.each do |image_url|
|
61
|
+
html.gsub!(image_url, "./#{Image::IMAGE_DIR}/#{File.basename(image_url)}")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
|
3
|
+
module QiitaExport
|
4
|
+
class Config
|
5
|
+
|
6
|
+
HOME_CONFIG_FILE = "~/.qiita-exportrc"
|
7
|
+
LOCAL_CONFIG_FILE = "./.qiita-exportrc"
|
8
|
+
|
9
|
+
DEFAULT_USER_AGENT = "QiitaExport Gem #{QiitaExport::VERSION}"
|
10
|
+
|
11
|
+
DEFAULT_HEADER = {
|
12
|
+
"User-Agent" => DEFAULT_USER_AGENT
|
13
|
+
}
|
14
|
+
|
15
|
+
DEFAULT_KOBITO_DB = "~/Library/Containers/com.qiita.Kobito/Data/Library/Kobito/Kobito.db"
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def configure(argv)
|
19
|
+
@option = {}
|
20
|
+
@parser = OptionParser.new do |opt|
|
21
|
+
opt.version = QiitaExport::VERSION
|
22
|
+
opt.on('-u', '--url=url', 'specify the URL for the Qiita(or Qiita Team).') { |v| @option[:url] = v }
|
23
|
+
opt.on('-l', '--url-list=filepath', 'specify the file path of the URL list.') { |v| @option[:'url-list'] = v }
|
24
|
+
opt.on('-U', '--user-id=user_id', 'specify the userid for the Qiita.') { |v| @option[:'user-id'] = v }
|
25
|
+
opt.on('-k', '--kobito=[Kobito.db]', 'export Kobito.app database.') { |v| @option[:kobito] = v }
|
26
|
+
opt.on('-t', '--team=teamname', 'export Qiita Team articles only.') { |v| @option[:team] = v }
|
27
|
+
opt.on('-i', '--image', 'export with images.') { |v| @option[:image] = v }
|
28
|
+
opt.on('-h', '--html', 'export in html format(experimental).') { |v| @option[:html] = v }
|
29
|
+
opt.on('-o', '--output-dir=dirpath', 'specify the full path of destination directory.') { |v| @option[:'output-dir'] = v }
|
30
|
+
opt.on('-a', '--api-token=token', 'specify API token for Qiita.') { |v| @option[:'api-token'] = v }
|
31
|
+
end
|
32
|
+
|
33
|
+
# load home config
|
34
|
+
@parser.load(File.expand_path(HOME_CONFIG_FILE))
|
35
|
+
|
36
|
+
# load local config
|
37
|
+
@parser.load(File.expand_path(LOCAL_CONFIG_FILE))
|
38
|
+
|
39
|
+
# parse argv
|
40
|
+
@parser.parse!(argv)
|
41
|
+
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def empty?
|
46
|
+
@option.empty?
|
47
|
+
end
|
48
|
+
|
49
|
+
def help
|
50
|
+
@parser.help
|
51
|
+
end
|
52
|
+
|
53
|
+
def validate!
|
54
|
+
if present?(@option[:'url-list']) && !File.exist?(@option[:'url-list'])
|
55
|
+
fail ArgumentError.new("-l (#{@option[:'url-list']}) does not exist.")
|
56
|
+
end
|
57
|
+
|
58
|
+
if kobito? && !File.exist?(kobito_db)
|
59
|
+
fail ArgumentError.new("#{kobito_db} does not exist.")
|
60
|
+
end
|
61
|
+
|
62
|
+
if kobito? && api?
|
63
|
+
fail ArgumentError.new("if you specify option --kobito, you cannot specify option --url, --url-list and --user-id.")
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def kobito?
|
68
|
+
@option.key?(:kobito)
|
69
|
+
end
|
70
|
+
|
71
|
+
def api?
|
72
|
+
present?(@option[:url]) || present?(@option[:'url-list']) || present?(@option[:'user-id'])
|
73
|
+
end
|
74
|
+
|
75
|
+
def user?
|
76
|
+
present?(@option[:'user-id'])
|
77
|
+
end
|
78
|
+
|
79
|
+
def team?
|
80
|
+
present?(@option[:team])
|
81
|
+
end
|
82
|
+
|
83
|
+
def file_export?
|
84
|
+
present?(@option[:'output-dir'])
|
85
|
+
end
|
86
|
+
|
87
|
+
def html_export?
|
88
|
+
@option[:html]
|
89
|
+
end
|
90
|
+
|
91
|
+
def image_export?
|
92
|
+
@option[:image]
|
93
|
+
end
|
94
|
+
|
95
|
+
def team_url?(url)
|
96
|
+
url !~ /^https?:\/\/qiita\.com/
|
97
|
+
end
|
98
|
+
|
99
|
+
DOMAIN_PATTERN = Regexp.new("https?://([^/]+)/")
|
100
|
+
def team_name(url = nil)
|
101
|
+
if (kobito? || user?) && team?
|
102
|
+
@option[:team]
|
103
|
+
elsif api? && team_url?(url)
|
104
|
+
url.match(DOMAIN_PATTERN)[1].split('.')[0]
|
105
|
+
else
|
106
|
+
""
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def article_urls
|
111
|
+
urls = []
|
112
|
+
urls << @option[:url] if present?(@option[:url])
|
113
|
+
|
114
|
+
if present?(@option[:'url-list'])
|
115
|
+
open(@option[:'url-list']) { |io|
|
116
|
+
io.each_line { |line|
|
117
|
+
url = line.chop
|
118
|
+
next if blank?(url)
|
119
|
+
urls << url
|
120
|
+
}
|
121
|
+
}
|
122
|
+
end
|
123
|
+
urls
|
124
|
+
end
|
125
|
+
|
126
|
+
def kobito_db
|
127
|
+
if blank?(@option[:kobito])
|
128
|
+
File.expand_path(DEFAULT_KOBITO_DB)
|
129
|
+
else
|
130
|
+
File.expand_path(@option[:kobito])
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def api_token
|
135
|
+
@option[:'api-token']
|
136
|
+
end
|
137
|
+
|
138
|
+
def has_api_token?
|
139
|
+
present?(@option[:'api-token'])
|
140
|
+
end
|
141
|
+
|
142
|
+
def default_header
|
143
|
+
DEFAULT_HEADER.clone
|
144
|
+
end
|
145
|
+
|
146
|
+
def auth_header
|
147
|
+
header = default_header
|
148
|
+
header["Authorization"] = "Bearer #{api_token}"
|
149
|
+
header
|
150
|
+
end
|
151
|
+
|
152
|
+
def user_id
|
153
|
+
@option[:'user-id']
|
154
|
+
end
|
155
|
+
|
156
|
+
def export_dir_path
|
157
|
+
if user?
|
158
|
+
File.join(File.expand_path(@option[:'output-dir'].strip), user_id)
|
159
|
+
else
|
160
|
+
File.expand_path(@option[:'output-dir'].strip)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def filename(title)
|
165
|
+
return "" if !html_export? && blank?(title)
|
166
|
+
if html_export?
|
167
|
+
"index.html"
|
168
|
+
else
|
169
|
+
"#{title.gsub(/\//, ':')}.md"
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def blank?(str)
|
176
|
+
str.nil? || str.empty?
|
177
|
+
end
|
178
|
+
|
179
|
+
def present?(str)
|
180
|
+
!blank?(str)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
module QiitaExport
|
4
|
+
class Exporter
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
end
|
8
|
+
|
9
|
+
def export
|
10
|
+
fetcher = create_fetcher
|
11
|
+
articles = fetcher.find_articles
|
12
|
+
if (Config.file_export?)
|
13
|
+
export_file(articles)
|
14
|
+
else
|
15
|
+
export_console(articles)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def create_fetcher
|
22
|
+
if Config.kobito?
|
23
|
+
QiitaExport::Fetcher::KobitoFetcher.new
|
24
|
+
elsif Config.user?
|
25
|
+
QiitaExport::Fetcher::UserFetcher.new
|
26
|
+
else
|
27
|
+
QiitaExport::Fetcher::UrlFetcher.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def export_console(articles)
|
32
|
+
articles.each do |article|
|
33
|
+
$stdout.puts("key: #{article.key} title: #{article.title} url: #{article.url} created_at: #{article.created_at} updated_at: #{article.updated_at}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def export_file(articles)
|
38
|
+
$stdout.puts "export articles to #{Config.export_dir_path}"
|
39
|
+
FileUtils.makedirs(Config.export_dir_path) unless Dir.exists?(Config.export_dir_path)
|
40
|
+
articles.each do |article|
|
41
|
+
article.save
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'json'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module QiitaExport::Fetcher
|
6
|
+
class ApiFetcher < Base
|
7
|
+
|
8
|
+
DEFAULT_HOST = "qiita.com"
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def to_article(res)
|
13
|
+
key = res['id']
|
14
|
+
url = res['url']
|
15
|
+
title = res['title']
|
16
|
+
raw_body = res['body']
|
17
|
+
rendered_body = res['rendered_body']
|
18
|
+
created_at = res['created_at']
|
19
|
+
updated_at = res['updated_at']
|
20
|
+
user_id = res['user_id']
|
21
|
+
|
22
|
+
::QiitaExport::Article.new(key, url, title, raw_body, rendered_body, created_at, updated_at, user_id)
|
23
|
+
end
|
24
|
+
|
25
|
+
def article_key(url)
|
26
|
+
File.basename(url)
|
27
|
+
end
|
28
|
+
|
29
|
+
def request_header
|
30
|
+
has_api_token? ? auth_header : default_header
|
31
|
+
end
|
32
|
+
|
33
|
+
def api_domain(url = nil)
|
34
|
+
if url.nil?
|
35
|
+
team? ? "#{team_name}.#{DEFAULT_HOST}" : DEFAULT_HOST
|
36
|
+
else
|
37
|
+
URI.parse(url).host
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module QiitaExport::Fetcher
|
5
|
+
class Base
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@config, :kobito?, :kobito_db, :file_export?,
|
9
|
+
:export_dir_path, :team?, :team_name,
|
10
|
+
:article_urls, :has_api_token?, :api_token,
|
11
|
+
:user_id, :default_header, :auth_header
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@config = ::QiitaExport::Config
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_articles
|
18
|
+
raise NotImplementedError.new("You must implement #{self.class}##{__method__}")
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'sqlite3'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module QiitaExport::Fetcher
|
7
|
+
class KobitoFetcher < Base
|
8
|
+
|
9
|
+
SELECT =<<-"EOS"
|
10
|
+
SELECT
|
11
|
+
i.zurl,
|
12
|
+
i.ztitle,
|
13
|
+
i.zraw_body,
|
14
|
+
i.zbody,
|
15
|
+
i.zposted_at,
|
16
|
+
i.zcreated_at,
|
17
|
+
i.zupdated_at,
|
18
|
+
i.zupdated_at_on_qiita,
|
19
|
+
t.zurl_name
|
20
|
+
FROM
|
21
|
+
zitem i left outer join zteam t on i.zteam = t.z_pk
|
22
|
+
WHERE
|
23
|
+
zin_trash is null
|
24
|
+
EOS
|
25
|
+
|
26
|
+
ORDER = " ORDER BY i.zcreated_at"
|
27
|
+
|
28
|
+
def find_articles
|
29
|
+
where = if team?
|
30
|
+
" and zurl_name = :team"
|
31
|
+
else
|
32
|
+
" and zteam is null"
|
33
|
+
end
|
34
|
+
query = "#{SELECT}#{where}#{ORDER}"
|
35
|
+
|
36
|
+
db = SQLite3::Database.new(kobito_db)
|
37
|
+
db.results_as_hash = true
|
38
|
+
|
39
|
+
stmt = db.prepare(query)
|
40
|
+
stmt.bind_param("team", team_name) if team?
|
41
|
+
rs = stmt.execute
|
42
|
+
|
43
|
+
articles = []
|
44
|
+
while(row = rs.next)
|
45
|
+
articles << to_article(row)
|
46
|
+
end
|
47
|
+
articles
|
48
|
+
ensure
|
49
|
+
stmt.close if stmt
|
50
|
+
db.close if db
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def to_article(row)
|
56
|
+
key = File.basename(row['ZURL'])
|
57
|
+
url = row['ZURL']
|
58
|
+
title = row['ZTITLE']
|
59
|
+
raw_body = row['ZRAW_BODY']
|
60
|
+
rendered_body = row['ZBODY']
|
61
|
+
|
62
|
+
zposted_at = convert_timestamp(row['ZPOSTED_AT'])
|
63
|
+
zcreated_at = convert_timestamp(row['ZCREATED_AT'])
|
64
|
+
zupdated_at_on_qiita = convert_timestamp(row['ZUPDATED_AT_ON_QIITA'])
|
65
|
+
zupdated_at = convert_timestamp(row['ZUPDATED_AT'])
|
66
|
+
|
67
|
+
# puts "key: #{key} zposted_at: #{zposted_at} zcreated_at: #{zcreated_at} zupdated_at_on_qiita: #{zupdated_at_on_qiita} zupdated_at: #{zupdated_at}"
|
68
|
+
::QiitaExport::Article.new(key, url, title, raw_body, rendered_body, zcreated_at, zupdated_at, 'kobito_user')
|
69
|
+
end
|
70
|
+
|
71
|
+
BASE_DATE = Time.new(2001, 1, 1, 0, 0, 0, 0).to_i
|
72
|
+
def convert_timestamp(timestamp)
|
73
|
+
Time.at(timestamp + BASE_DATE).iso8601
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'json'
|
3
|
+
require 'open-uri'
|
4
|
+
|
5
|
+
module QiitaExport::Fetcher
|
6
|
+
class UrlFetcher < ApiFetcher
|
7
|
+
|
8
|
+
def find_articles
|
9
|
+
articles = []
|
10
|
+
article_urls.each do |url|
|
11
|
+
articles << to_article(find_article(url))
|
12
|
+
end
|
13
|
+
articles
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def find_article(url)
|
19
|
+
open("https://#{api_domain(url)}/api/v2/items/#{article_key(url)}", request_header) do |io|
|
20
|
+
JSON.parse(io.read)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'json'
|
3
|
+
require 'open-uri'
|
4
|
+
|
5
|
+
module QiitaExport::Fetcher
|
6
|
+
class UserFetcher < ApiFetcher
|
7
|
+
|
8
|
+
PER_PAGE = 100
|
9
|
+
|
10
|
+
def find_articles
|
11
|
+
articles = []
|
12
|
+
page = 1
|
13
|
+
while true
|
14
|
+
user_articles = find_user_articles(page)
|
15
|
+
break if user_articles.empty?
|
16
|
+
|
17
|
+
user_articles.each do |user_article|
|
18
|
+
articles << to_article(user_article)
|
19
|
+
end
|
20
|
+
|
21
|
+
page += 1
|
22
|
+
sleep(0.3)
|
23
|
+
end
|
24
|
+
articles.sort { |a, b| a.created_at <=> b.created_at }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def find_user_articles(page)
|
30
|
+
open("https://#{api_domain}/api/v2/users/#{user_id}/items?page=#{page}&per_page=#{PER_PAGE}", request_header) do |io|
|
31
|
+
JSON.parse(io.read)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
require 'fileutils'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module QiitaExport
|
6
|
+
class Image
|
7
|
+
attr_reader :key, :url
|
8
|
+
|
9
|
+
IMAGE_DIR = "images"
|
10
|
+
|
11
|
+
def initialize(key, url)
|
12
|
+
@key = key
|
13
|
+
@url = url
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.extract_urls(key, markdown)
|
17
|
+
image_urls = markdown.scan(/!\[.*?\]\((.+?)(?: \".*?\")?\)/).flatten
|
18
|
+
image_urls.map do |image_url|
|
19
|
+
new(key, image_url)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def require_auth?
|
24
|
+
@url !~ /qiita-image-store.s3.amazonaws.com/
|
25
|
+
end
|
26
|
+
|
27
|
+
def request_header
|
28
|
+
require_auth? ? Config.auth_header : Config.default_header
|
29
|
+
end
|
30
|
+
|
31
|
+
def filename
|
32
|
+
"#{File.basename(url)}"
|
33
|
+
end
|
34
|
+
|
35
|
+
def save(path)
|
36
|
+
image_dir = File.join(File.expand_path(path), IMAGE_DIR)
|
37
|
+
|
38
|
+
FileUtils.makedirs(image_dir) unless Dir.exists?(image_dir)
|
39
|
+
|
40
|
+
file_path = File.join(image_dir, filename)
|
41
|
+
|
42
|
+
open(file_path, 'wb') do |out|
|
43
|
+
open(@url, request_header) do |image|
|
44
|
+
out.write(image.read)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'qiita-export/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "qiita-export"
|
8
|
+
spec.version = QiitaExport::VERSION
|
9
|
+
spec.authors = ["Shin Akiyama"]
|
10
|
+
spec.email = ["akishin999@gmail.com"]
|
11
|
+
spec.summary = %q{export tool for Qiita}
|
12
|
+
spec.description = %q{export tool for Qiita}
|
13
|
+
spec.homepage = "https://github.com/akishin/qiita-export"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
spec.bindir = 'bin'
|
21
|
+
|
22
|
+
spec.add_dependency "sqlite3"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec", "3.4.0"
|
27
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.expand_path(File.join('..', 'spec_helper'), File.dirname(__FILE__))
|
4
|
+
require 'qiita-export'
|
5
|
+
|
6
|
+
describe QiitaExport::Config do
|
7
|
+
let(:valid_url) { 'http://qiita.com/akishin/items/61630d628f4c8e141ef2' }
|
8
|
+
let(:valid_kobito_db) { File.expand_path(QiitaExport::Config::DEFAULT_KOBITO_DB) }
|
9
|
+
let(:valid_team_name) { 'example' }
|
10
|
+
let(:valid_argv) {
|
11
|
+
[
|
12
|
+
'--url', valid_url,
|
13
|
+
'--kobito', valid_kobito_db,
|
14
|
+
'--image',
|
15
|
+
'--html',
|
16
|
+
'--team', valid_team_name,
|
17
|
+
]
|
18
|
+
}
|
19
|
+
|
20
|
+
describe ".configure" do
|
21
|
+
|
22
|
+
context "with arguments" do
|
23
|
+
let(:arguments) do
|
24
|
+
valid_argv
|
25
|
+
end
|
26
|
+
|
27
|
+
subject do
|
28
|
+
described_class.configure(arguments)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns a QiitaExport::Config" do
|
32
|
+
expect(subject).to eq(QiitaExport::Config)
|
33
|
+
end
|
34
|
+
it { expect(subject.article_urls).to include(valid_url) }
|
35
|
+
it { expect(subject.kobito_db).to eq(valid_kobito_db) }
|
36
|
+
it { expect(subject.api?).to be_truthy }
|
37
|
+
it { expect(subject.image_export?).to be_truthy }
|
38
|
+
it { expect(subject.html_export?).to be_truthy }
|
39
|
+
it { expect(subject.team?).to be_truthy }
|
40
|
+
it { expect(subject.team_name).to eq(valid_team_name) }
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
|
3
|
+
require File.expand_path(File.join('..', 'spec_helper'), File.dirname(__FILE__))
|
4
|
+
require 'qiita-export'
|
5
|
+
|
6
|
+
describe QiitaExport::Image do
|
7
|
+
let(:valid_key) { '99999999999999999999' }
|
8
|
+
let(:valid_url) { 'http://qiita-image-store.s3.amazonaws.com/example.jpg' }
|
9
|
+
|
10
|
+
describe ".new" do
|
11
|
+
|
12
|
+
context "with arguments" do
|
13
|
+
let(:arguments) do
|
14
|
+
[valid_key, valid_url]
|
15
|
+
end
|
16
|
+
|
17
|
+
subject do
|
18
|
+
described_class.new(*arguments)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "returns a QiitaExport::Image" do
|
22
|
+
expect(subject).to be_an_instance_of(QiitaExport::Image)
|
23
|
+
end
|
24
|
+
it { expect(subject.key).to eq(valid_key) }
|
25
|
+
it { expect(subject.url).to eq(valid_url) }
|
26
|
+
end
|
27
|
+
|
28
|
+
context "without any arguments" do
|
29
|
+
let(:arguments) do
|
30
|
+
[]
|
31
|
+
end
|
32
|
+
|
33
|
+
it "raise ArgumentError" do
|
34
|
+
expect {
|
35
|
+
described_class.new(*arguments)
|
36
|
+
}.to raise_error(ArgumentError)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
let(:include_images) {
|
42
|
+
<<-"EOS"
|
43
|
+
## Example Markdown
|
44
|
+

|
45
|
+

|
46
|
+
EOS
|
47
|
+
}
|
48
|
+
|
49
|
+
let(:not_include_images) {
|
50
|
+
<<-"EOS"
|
51
|
+
## Example Markdown
|
52
|
+
### Test1
|
53
|
+
### Test2
|
54
|
+
EOS
|
55
|
+
}
|
56
|
+
|
57
|
+
describe ".extract_urls" do
|
58
|
+
context "when there is image urls" do
|
59
|
+
subject do
|
60
|
+
described_class.extract_urls(valid_key, include_images)
|
61
|
+
end
|
62
|
+
|
63
|
+
it { expect(subject).not_to be_empty }
|
64
|
+
it { expect(subject.length).to eq(2) }
|
65
|
+
it { expect(subject.first).to be_an_instance_of(QiitaExport::Image) }
|
66
|
+
it { expect(subject.first.url).to eq('https://example.com/foo.png') }
|
67
|
+
it { expect(subject.last).to be_an_instance_of(QiitaExport::Image) }
|
68
|
+
it { expect(subject.last.url).to eq('https://example.com/bar.png') }
|
69
|
+
end
|
70
|
+
|
71
|
+
context "when there is no image urls" do
|
72
|
+
subject do
|
73
|
+
described_class.extract_urls(valid_key, not_include_images)
|
74
|
+
end
|
75
|
+
it { expect(subject).to be_empty }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: qiita-export
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Shin Akiyama
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-19 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: sqlite3
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 3.4.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 3.4.0
|
69
|
+
description: export tool for Qiita
|
70
|
+
email:
|
71
|
+
- akishin999@gmail.com
|
72
|
+
executables:
|
73
|
+
- qiita-export
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- bin/qiita-export
|
83
|
+
- lib/qiita-export.rb
|
84
|
+
- lib/qiita-export/article.rb
|
85
|
+
- lib/qiita-export/config.rb
|
86
|
+
- lib/qiita-export/exporter.rb
|
87
|
+
- lib/qiita-export/fetcher/api_fetcher.rb
|
88
|
+
- lib/qiita-export/fetcher/base.rb
|
89
|
+
- lib/qiita-export/fetcher/kobito_fetcher.rb
|
90
|
+
- lib/qiita-export/fetcher/url_fetcher.rb
|
91
|
+
- lib/qiita-export/fetcher/user_fetcher.rb
|
92
|
+
- lib/qiita-export/image.rb
|
93
|
+
- lib/qiita-export/version.rb
|
94
|
+
- qiita-export.gemspec
|
95
|
+
- spec/qiita-export/config_spec.rb
|
96
|
+
- spec/qiita-export/image_spec.rb
|
97
|
+
- spec/spec_helper.rb
|
98
|
+
homepage: https://github.com/akishin/qiita-export
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubyforge_project:
|
118
|
+
rubygems_version: 2.4.5
|
119
|
+
signing_key:
|
120
|
+
specification_version: 4
|
121
|
+
summary: export tool for Qiita
|
122
|
+
test_files:
|
123
|
+
- spec/qiita-export/config_spec.rb
|
124
|
+
- spec/qiita-export/image_spec.rb
|
125
|
+
- spec/spec_helper.rb
|