wikiwiki 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -0
  3. data/README.ja.md +100 -3
  4. data/README.md +100 -3
  5. data/exe/wikiwiki +8 -0
  6. data/lib/wikiwiki/api.rb +42 -6
  7. data/lib/wikiwiki/auth/token.rb +23 -0
  8. data/lib/wikiwiki/auth.rb +6 -0
  9. data/lib/wikiwiki/cli/commands/attachment/delete.rb +34 -0
  10. data/lib/wikiwiki/cli/commands/attachment/get.rb +45 -0
  11. data/lib/wikiwiki/cli/commands/attachment/list.rb +37 -0
  12. data/lib/wikiwiki/cli/commands/attachment/put.rb +55 -0
  13. data/lib/wikiwiki/cli/commands/attachment/show.rb +44 -0
  14. data/lib/wikiwiki/cli/commands/auth.rb +32 -0
  15. data/lib/wikiwiki/cli/commands/base.rb +64 -0
  16. data/lib/wikiwiki/cli/commands/page/delete.rb +32 -0
  17. data/lib/wikiwiki/cli/commands/page/get.rb +46 -0
  18. data/lib/wikiwiki/cli/commands/page/list.rb +36 -0
  19. data/lib/wikiwiki/cli/commands/page/put.rb +44 -0
  20. data/lib/wikiwiki/cli/commands/page/show.rb +44 -0
  21. data/lib/wikiwiki/cli/formatter/json.rb +18 -0
  22. data/lib/wikiwiki/cli.rb +49 -0
  23. data/lib/wikiwiki/rate_limiter.rb +4 -12
  24. data/lib/wikiwiki/sliding_window.rb +1 -3
  25. data/lib/wikiwiki/version.rb +1 -1
  26. data/lib/wikiwiki/wiki.rb +22 -3
  27. data/lib/wikiwiki.rb +5 -1
  28. data/sig/dry/cli.rbs +9 -0
  29. data/sig/wikiwiki/api.rbs +5 -3
  30. data/sig/wikiwiki/auth/token.rbs +9 -0
  31. data/sig/wikiwiki/auth.rbs +2 -0
  32. data/sig/wikiwiki/cli/commands/attachment.rbs +87 -0
  33. data/sig/wikiwiki/cli/commands/auth.rbs +9 -0
  34. data/sig/wikiwiki/cli/commands/base.rbs +27 -0
  35. data/sig/wikiwiki/cli/commands/page.rbs +82 -0
  36. data/sig/wikiwiki/cli/formatter/json.rbs +9 -0
  37. data/sig/wikiwiki/cli.rbs +11 -0
  38. data/sig/wikiwiki/wiki.rbs +5 -1
  39. data/sig/wikiwiki.rbs +3 -0
  40. metadata +60 -8
  41. data/LICENSE.txt +0 -21
  42. data/mise.toml +0 -6
  43. data/rbs_collection.yaml +0 -12
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c94061d746d2242a2a78e735c43fa5fba37888231ba3a92e959aeafa5afaec3a
4
- data.tar.gz: c9c3f4c74090aa044e10e48b72b03ad2a7b1b58713fec6e0447a871439153c05
3
+ metadata.gz: 9cd510fe9ee02e6521a325c51ed5e586a41a904ea0758fc78b16ad66159c40d9
4
+ data.tar.gz: e3e57832169db7d9644cdcbd2d8c3495576e8ff221338f107b99cb7fde161991
5
5
  SHA512:
6
- metadata.gz: 9bae7372d114df3f32f4f60d3ae1b4664ff3581c30ba0215e5d705de733063b2d12faaa92c557439f29b1e9b9707e9c3993400e738b672125365b7be24d401c1
7
- data.tar.gz: f6e397e07040ba97328609f437a30a29be059c2df7c7051d78def34c204f6d5bf6005e618014893ffddcb36e20cc41db414eb1187435776a60217b2177c960ee
6
+ metadata.gz: d3e67b565e5d9414a111dea276ea16f23bffe307c5c02b9de97062ce24a2e926304929bf975d5144cc359190680748cf224aae6f0819c96a21924b076152ca99
7
+ data.tar.gz: 3bfa6e34164386c86b19912170bb450da3dd50b2e610e8d722a3568d433d2877185fec64011cc7f68e283c4f1523286311e832b4c64ffa2102473b37083d2243
data/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.7.0] - 2025-11-03
4
+
5
+ ### Added
6
+
7
+ - Page deletion support
8
+ - `Wiki#delete_page` method to delete pages
9
+ - `wikiwiki page delete` command in CLI
10
+ - Validation in `Wiki#update_page` and `wikiwiki page put` to prevent accidental deletion with empty content
11
+ - `ConflictError` exception class for HTTP 409 Conflict responses
12
+
13
+ ### Changed
14
+
15
+ - `attachment put --force` behavior: attempts upload first, then deletes and retries on conflict (409 error)
16
+ - Previously: checked existence first, then deleted and uploaded
17
+ - Now: optimistic upload pattern eliminates Time-of-Check-Time-of-Use (TOCTOU) race conditions
18
+
19
+ ## [0.6.0] - 2025-11-02
20
+
21
+ ### Added
22
+
23
+ - Pre-obtained token reuse support
24
+ - `Auth.token(token:)` for authentication with pre-obtained JWT tokens
25
+ - `wikiwiki auth` command to obtain authentication tokens
26
+ - `Wiki#token` method to retrieve the current authentication token
27
+ - `--token` option and `WIKIWIKI_TOKEN` environment variable support for all commands
28
+ - JWT token expiration validation with AuthenticationError for expired tokens
29
+ - Command-line interface (`wikiwiki` command) for all API operations
30
+ - Page commands: `list`, `show`, `get`, `put`
31
+ - Attachment commands: `list`, `show`, `get`, `put`, `delete`
32
+ - Authentication command: `auth`
33
+ - Environment variable support for credentials (WIKIWIKI_WIKI_ID, WIKIWIKI_TOKEN, WIKIWIKI_PASSWORD, WIKIWIKI_API_KEY_ID, WIKIWIKI_SECRET)
34
+ - JSON output option (`--json`) for automation
35
+ - Verbose (`--verbose`) and debug (`--debug`) modes
36
+ - File overwrite protection with `--force` flag
37
+ - Attachment size limit validation (512 KiB) for uploads
38
+
3
39
  ## [0.5.0] - 2025-10-31
4
40
 
5
41
  ### Added
data/README.ja.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  {file:README.md English version}
4
4
 
5
- [Wikiwiki](https://wikiwiki.jp/) REST API用のRubyクライアントライブラリです。
5
+ [Wikiwiki](https://wikiwiki.jp/) REST API用のRubyクライアントライブラリおよびコマンドラインツールです。
6
6
 
7
7
  ## 概要
8
8
 
9
- このgemは、Wikiwikiのwikiをプログラムから操作するためのシンプルなインターフェースを提供します。ページ操作(一覧取得、読み取り、書き込み)と添付ファイル管理(一覧取得、アップロード、ダウンロード、削除)をサポートしています。
9
+ このgemは、Wikiwikiのwikiを操作するためのRubyライブラリとCLIツールの両方を提供します。
10
10
 
11
11
  ## インストール
12
12
 
@@ -44,9 +44,95 @@ auth = Wikiwiki::Auth.password(password: "your_admin_password")
44
44
  auth = Wikiwiki::Auth.api_key(api_key_id: "your_api_key_id", secret: "your_secret")
45
45
  ```
46
46
 
47
+ ### トークン認証
48
+
49
+ 事前に認証して得られたJWTトークンは、有効期限以内なら再利用できます:
50
+
51
+ ```ruby
52
+ auth = Wikiwiki::Auth.token(token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
53
+ ```
54
+
47
55
  ## 使い方
48
56
 
49
- ### 基本的な使用例
57
+ ### コマンドラインインターフェース
58
+
59
+ `wikiwiki`コマンドで、すべてのAPI操作にアクセスできます:
60
+
61
+ ```bash
62
+ # 環境変数で認証情報を設定(オプション)
63
+ export WIKIWIKI_WIKI_ID=your-wiki-id
64
+ export WIKIWIKI_PASSWORD=your-password
65
+ # または
66
+ export WIKIWIKI_API_KEY_ID=your-api-key-id
67
+ export WIKIWIKI_SECRET=your-secret
68
+ # または事前に取得したトークンを使用
69
+ export WIKIWIKI_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
70
+
71
+ # 後で使用するためにトークンを取得
72
+ wikiwiki auth --wiki-id=your-wiki-id --password=your-password
73
+ # またはexportと組み合わせて使用
74
+ export WIKIWIKI_TOKEN=$(wikiwiki auth --wiki-id=your-wiki-id --password=your-password)
75
+
76
+ # ページ一覧
77
+ wikiwiki page list
78
+
79
+ # ページのメタデータ表示
80
+ wikiwiki page show FrontPage
81
+
82
+ # ページのコンテンツをダウンロード
83
+ wikiwiki page get FrontPage
84
+ wikiwiki page get FrontPage frontpage.txt
85
+
86
+ # ページをアップロード/更新(標準入力またはファイルから)
87
+ wikiwiki page put TestPage < content.txt
88
+ wikiwiki page put TestPage content.txt
89
+
90
+ # ページを削除
91
+ wikiwiki page delete TestPage
92
+
93
+ # 添付ファイル一覧
94
+ wikiwiki attachment list FrontPage
95
+
96
+ # 添付ファイルのメタデータ表示
97
+ wikiwiki attachment show FrontPage logo.png
98
+
99
+ # 添付ファイルをダウンロード
100
+ wikiwiki attachment get FrontPage logo.png
101
+ wikiwiki attachment get FrontPage logo.png --directory downloads/
102
+
103
+ # 添付ファイルをアップロード(最大512 KiB)
104
+ wikiwiki attachment put FrontPage image.png
105
+ wikiwiki attachment put FrontPage local.png --name remote.png
106
+
107
+ # 添付ファイルを削除
108
+ wikiwiki attachment delete FrontPage logo.png
109
+
110
+ # 既存のファイル/添付ファイルを上書きするには --force を使用
111
+ wikiwiki page get FrontPage existing.txt --force
112
+ wikiwiki attachment put FrontPage logo.png --force
113
+
114
+ # 注意: --force による添付ファイルの上書きはアトミックではありません。
115
+ # まずアップロードを試み、競合エラー(ファイルが既に存在)が発生した場合、
116
+ # 既存ファイルを削除してアップロードをリトライします。
117
+ # リトライに失敗した場合、添付ファイルは失われます。
118
+
119
+ # コマンドラインで認証情報を指定(環境変数より優先)
120
+ wikiwiki --wiki-id=your-wiki-id --password=your-password page list
121
+ wikiwiki --wiki-id=your-wiki-id --api-key-id=id --secret=secret page list
122
+ wikiwiki --wiki-id=your-wiki-id --token=eyJ... page list
123
+
124
+ # 自動化のためのJSON出力
125
+ wikiwiki page list --json
126
+ wikiwiki attachment show FrontPage logo.png --json
127
+
128
+ # 詳細モードとデバッグモード
129
+ wikiwiki page list --verbose
130
+ wikiwiki page list --debug
131
+ ```
132
+
133
+ ### Rubyライブラリ
134
+
135
+ ライブラリを使用する基本的な例:
50
136
 
51
137
  ```ruby
52
138
  require "wikiwiki"
@@ -55,6 +141,14 @@ require "wikiwiki"
55
141
  auth = Wikiwiki::Auth.password(password: "admin_password")
56
142
  wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
57
143
 
144
+ # 認証トークンを取得して再利用
145
+ token = wiki.token
146
+ # トークンを保存して後で使用...
147
+
148
+ # 後で保存したトークンを使用
149
+ auth = Wikiwiki::Auth.token(token: token)
150
+ wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
151
+
58
152
  # すべてのページ名の一覧を取得
59
153
  page_names = wiki.page_names
60
154
  # => ["FrontPage", "SideBar", ...]
@@ -70,6 +164,9 @@ wiki.update_page(page_name: "TestPage", source: <<~SOURCE)
70
164
  # Hello World
71
165
  SOURCE
72
166
 
167
+ # ページを削除
168
+ wiki.delete_page(page_name: "TestPage")
169
+
73
170
  # 添付ファイル名の一覧を取得
74
171
  attachment_names = wiki.attachment_names(page_name: "FrontPage")
75
172
 
data/README.md CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  {file:README.ja.md 日本語版}
4
4
 
5
- A Ruby client library for the [Wikiwiki](https://wikiwiki.jp/) REST API.
5
+ A Ruby client library and command-line interface for the [Wikiwiki](https://wikiwiki.jp/) REST API.
6
6
 
7
7
  ## Overview
8
8
 
9
- This gem provides a simple interface to interact with Wikiwiki wikis programmatically. It supports page operations (list, read, write) and attachment management (list, upload, download, delete).
9
+ This gem provides both a Ruby library and CLI tool to interact with Wikiwiki wikis.
10
10
 
11
11
  ## Installation
12
12
 
@@ -44,9 +44,95 @@ auth = Wikiwiki::Auth.password(password: "your_admin_password")
44
44
  auth = Wikiwiki::Auth.api_key(api_key_id: "your_api_key_id", secret: "your_secret")
45
45
  ```
46
46
 
47
+ ### Token Authentication
48
+
49
+ A JWT token obtained from a previous authentication can be reused within its validity period:
50
+
51
+ ```ruby
52
+ auth = Wikiwiki::Auth.token(token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...")
53
+ ```
54
+
47
55
  ## Usage
48
56
 
49
- ### Basic Example
57
+ ### Command Line Interface
58
+
59
+ The `wikiwiki` command provides access to all API operations:
60
+
61
+ ```bash
62
+ # Set credentials via environment variables (optional)
63
+ export WIKIWIKI_WIKI_ID=your-wiki-id
64
+ export WIKIWIKI_PASSWORD=your-password
65
+ # or
66
+ export WIKIWIKI_API_KEY_ID=your-api-key-id
67
+ export WIKIWIKI_SECRET=your-secret
68
+ # or use a pre-obtained token
69
+ export WIKIWIKI_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
70
+
71
+ # Obtain a token for later use
72
+ wikiwiki auth --wiki-id=your-wiki-id --password=your-password
73
+ # or combine with export
74
+ export WIKIWIKI_TOKEN=$(wikiwiki auth --wiki-id=your-wiki-id --password=your-password)
75
+
76
+ # List pages
77
+ wikiwiki page list
78
+
79
+ # Show page metadata
80
+ wikiwiki page show FrontPage
81
+
82
+ # Download page content
83
+ wikiwiki page get FrontPage
84
+ wikiwiki page get FrontPage frontpage.txt
85
+
86
+ # Upload/update page (from stdin or file)
87
+ wikiwiki page put TestPage < content.txt
88
+ wikiwiki page put TestPage content.txt
89
+
90
+ # Delete page
91
+ wikiwiki page delete TestPage
92
+
93
+ # List attachments
94
+ wikiwiki attachment list FrontPage
95
+
96
+ # Show attachment metadata
97
+ wikiwiki attachment show FrontPage logo.png
98
+
99
+ # Download attachment
100
+ wikiwiki attachment get FrontPage logo.png
101
+ wikiwiki attachment get FrontPage logo.png --directory downloads/
102
+
103
+ # Upload attachment (max 512 KiB)
104
+ wikiwiki attachment put FrontPage image.png
105
+ wikiwiki attachment put FrontPage local.png --name remote.png
106
+
107
+ # Delete attachment
108
+ wikiwiki attachment delete FrontPage logo.png
109
+
110
+ # Use --force to overwrite existing files/attachments
111
+ wikiwiki page get FrontPage existing.txt --force
112
+ wikiwiki attachment put FrontPage logo.png --force
113
+
114
+ # Note: Attachment overwrite with --force is not atomic.
115
+ # First attempts to upload; if it fails due to conflict (file exists),
116
+ # deletes the existing file and retries the upload.
117
+ # If retry fails, the attachment will be lost.
118
+
119
+ # Authentication via command line (overrides environment variables)
120
+ wikiwiki --wiki-id=your-wiki-id --password=your-password page list
121
+ wikiwiki --wiki-id=your-wiki-id --api-key-id=id --secret=secret page list
122
+ wikiwiki --wiki-id=your-wiki-id --token=eyJ... page list
123
+
124
+ # JSON output for automation
125
+ wikiwiki page list --json
126
+ wikiwiki attachment show FrontPage logo.png --json
127
+
128
+ # Verbose and debug modes
129
+ wikiwiki page list --verbose
130
+ wikiwiki page list --debug
131
+ ```
132
+
133
+ ### Ruby Library
134
+
135
+ Basic example using the library:
50
136
 
51
137
  ```ruby
52
138
  require "wikiwiki"
@@ -55,6 +141,14 @@ require "wikiwiki"
55
141
  auth = Wikiwiki::Auth.password(password: "admin_password")
56
142
  wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
57
143
 
144
+ # Obtain and reuse authentication token
145
+ token = wiki.token
146
+ # Save token for later use...
147
+
148
+ # Later, use the saved token
149
+ auth = Wikiwiki::Auth.token(token: token)
150
+ wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
151
+
58
152
  # List all page names
59
153
  page_names = wiki.page_names
60
154
  # => ["FrontPage", "SideBar", ...]
@@ -70,6 +164,9 @@ wiki.update_page(page_name: "TestPage", source: <<~SOURCE)
70
164
  # Hello World
71
165
  SOURCE
72
166
 
167
+ # Delete a page
168
+ wiki.delete_page(page_name: "TestPage")
169
+
73
170
  # List attachment names
74
171
  attachment_names = wiki.attachment_names(page_name: "FrontPage")
75
172
 
data/exe/wikiwiki ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "wikiwiki"
5
+
6
+ cli = Wikiwiki::CLI.new
7
+ exit_code = cli.run(ARGV)
8
+ exit(exit_code)
data/lib/wikiwiki/api.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "jwt"
4
5
  require "net/http"
5
6
  require "uri"
6
7
 
@@ -13,6 +14,7 @@ module Wikiwiki
13
14
  # pages = api.get_pages
14
15
  class API
15
16
  attr_reader :logger
17
+ attr_reader :token
16
18
 
17
19
  BASE_URL = URI.parse("https://api.wikiwiki.jp").freeze
18
20
  private_constant :BASE_URL
@@ -60,6 +62,7 @@ module Wikiwiki
60
62
  # @param source [String] the page source content
61
63
  # @return [void]
62
64
  # @raise [Error] if request fails
65
+ # @note Passing an empty string as source will delete the page
63
66
  def put_page(encoded_page_name:, source:)
64
67
  uri = BASE_URL + "/#{wiki_id}/page/#{encoded_page_name}"
65
68
  response = request(:put, uri, body: {"source" => source})
@@ -123,14 +126,44 @@ module Wikiwiki
123
126
 
124
127
  # Authenticate with the Wikiwiki API
125
128
  #
126
- # @param auth [Auth::Password, Auth::ApiKey] authentication credentials
129
+ # @param auth [Auth::Password, Auth::ApiKey, Auth::Token] authentication credentials
127
130
  # @return [String] JWT token
128
131
  # @raise [Error] if authentication fails
129
132
  private def authenticate(auth)
130
- uri = BASE_URL + "/#{wiki_id}/auth"
131
- response = request(:post, uri, body: auth.to_h, authenticate: false)
132
- data = parse_json_response(response)
133
- data["token"]
133
+ token = if auth.is_a?(Auth::Token)
134
+ auth.token
135
+ else
136
+ uri = BASE_URL + "/#{wiki_id}/auth"
137
+ response = request(:post, uri, body: auth.to_h, authenticate: false)
138
+ data = parse_json_response(response)
139
+ data["token"]
140
+ end
141
+
142
+ validate_token_expiry(token) if auth.is_a?(Auth::Token)
143
+ token
144
+ end
145
+
146
+ # Validate JWT token expiry
147
+ #
148
+ # @param token [String] JWT token
149
+ # @return [void]
150
+ # @raise [AuthenticationError] if token has expired
151
+ private def validate_token_expiry(token)
152
+ payload, = JWT.decode(token, nil, false)
153
+ exp = payload["exp"]
154
+
155
+ if exp
156
+ exp_time = Time.at(exp)
157
+ if Time.now >= exp_time
158
+ raise AuthenticationError, "Token has expired at #{exp_time.iso8601}"
159
+ end
160
+
161
+ logger.debug("[#{wiki_id}] Token expires at: #{exp_time.iso8601}")
162
+ else
163
+ logger.debug("[#{wiki_id}] Token has no expiration")
164
+ end
165
+ rescue JWT::DecodeError => e
166
+ logger.debug("[#{wiki_id}] Failed to decode token: #{e.message}")
134
167
  end
135
168
 
136
169
  # Parse JSON response
@@ -139,6 +172,7 @@ module Wikiwiki
139
172
  # @return [Hash] parsed response body
140
173
  # @raise [AuthenticationError] if authentication fails (401)
141
174
  # @raise [ResourceNotFoundError] if resource not found (404)
175
+ # @raise [ConflictError] if conflict occurs (409)
142
176
  # @raise [ServerError] if server error (5xx)
143
177
  # @raise [APIError] if other API request fails
144
178
  private def parse_json_response(response)
@@ -149,6 +183,8 @@ module Wikiwiki
149
183
  raise AuthenticationError, message
150
184
  when 404
151
185
  raise ResourceNotFoundError, message
186
+ when 409
187
+ raise ConflictError, message
152
188
  when 500..599
153
189
  raise ServerError, message
154
190
  else
@@ -217,6 +253,6 @@ module Wikiwiki
217
253
  end
218
254
  end
219
255
 
220
- private attr_reader :wiki_id, :token
256
+ private attr_reader :wiki_id
221
257
  end
222
258
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ module Auth
5
+ # Token-based authentication using a pre-obtained JWT token
6
+ #
7
+ # This allows using a previously obtained authentication token without
8
+ # re-authenticating with password or API key credentials.
9
+ #
10
+ # @example
11
+ # token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
12
+ # auth = Wikiwiki::Auth.token(token: token)
13
+ # wiki = Wikiwiki::Wiki.new(wiki_id: "my-wiki", auth: auth)
14
+ Token = Data.define(:token)
15
+
16
+ class Token
17
+ # Reopen the class to add YARD documentation for attributes
18
+
19
+ # @!attribute [r] token
20
+ # @return [String] the JWT token
21
+ end
22
+ end
23
+ end
data/lib/wikiwiki/auth.rb CHANGED
@@ -17,5 +17,11 @@ module Wikiwiki
17
17
  # @param secret [String] secret key
18
18
  # @return [ApiKey] API key authentication object
19
19
  def self.api_key(api_key_id:, secret:) = ApiKey.new(api_key_id:, secret:)
20
+
21
+ # Create token-based authentication credentials
22
+ #
23
+ # @param token [String] JWT authentication token
24
+ # @return [Token] token authentication object
25
+ def self.token(token:) = Token.new(token:)
20
26
  end
21
27
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ class CLI
5
+ module Commands
6
+ module Attachment
7
+ # Delete attachment from a page
8
+ class Delete < Base
9
+ desc "Delete attachment from a page"
10
+
11
+ argument :page_name, required: true, desc: "Page name"
12
+ argument :file_name, required: true, desc: "Attachment file name"
13
+
14
+ # Execute the delete command
15
+ #
16
+ # @param page_name [String] name of the page
17
+ # @param file_name [String] name of the attachment file to delete
18
+ # @param out [IO] output stream
19
+ # @param err [IO] error stream
20
+ # @return [void]
21
+ def call(page_name:, file_name:, out: $stdout, err: $stderr, **)
22
+ wiki = create_wiki(out:, err:, **)
23
+
24
+ wiki.delete_attachment(page_name:, attachment_name: file_name)
25
+
26
+ say("Attachment '#{file_name}' deleted from page '#{page_name}'", out:, **)
27
+ end
28
+ end
29
+ end
30
+
31
+ register "attachment delete", Attachment::Delete
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ class CLI
5
+ module Commands
6
+ module Attachment
7
+ # Download attachment from a page
8
+ class Get < Base
9
+ desc "Download attachment from a page"
10
+
11
+ argument :page_name, required: true, desc: "Page name"
12
+ argument :file_name, required: true, desc: "Attachment file name"
13
+ option :directory, aliases: ["-d"], desc: "Download directory (current directory if omitted)"
14
+ option :force, aliases: ["-f"], type: :boolean, default: false, desc: "Overwrite existing file"
15
+
16
+ # Execute the get command
17
+ #
18
+ # @param page_name [String] name of the page
19
+ # @param file_name [String] name of the attachment file
20
+ # @param directory [String, nil] optional download directory
21
+ # @param force [Boolean] whether to overwrite existing file
22
+ # @param out [IO] output stream
23
+ # @param err [IO] error stream
24
+ # @return [void]
25
+ def call(page_name:, file_name:, directory: nil, force: false, out: $stdout, err: $stderr, **)
26
+ wiki = create_wiki(out:, err:, **)
27
+
28
+ output_path = directory ? File.join(directory, file_name) : file_name
29
+
30
+ if File.exist?(output_path) && !force
31
+ raise ArgumentError, "File '#{output_path}' already exists. Use --force to overwrite."
32
+ end
33
+
34
+ attachment = wiki.attachment(page_name:, attachment_name: file_name)
35
+ File.binwrite(output_path, attachment.content)
36
+
37
+ say("Attachment '#{file_name}' (#{attachment.content.bytesize} bytes) saved to #{output_path}", out:, **)
38
+ end
39
+ end
40
+ end
41
+
42
+ register "attachment get", Attachment::Get
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ class CLI
5
+ module Commands
6
+ # Attachment-related commands
7
+ module Attachment
8
+ # List attachments on a page
9
+ class List < Base
10
+ desc "List attachments on a page"
11
+
12
+ argument :page_name, required: true, desc: "Page name"
13
+ option :json, aliases: ["-j"], type: :boolean, default: false, desc: "Output as JSON"
14
+
15
+ # Execute the list command
16
+ #
17
+ # @param page_name [String] name of the page
18
+ # @param options [Hash] command options including wiki_id, auth, json, verbose, out, err
19
+ # @return [void]
20
+ def call(page_name:, out: $stdout, err: $stderr, **options)
21
+ wiki = create_wiki(out:, err:, **options)
22
+ attachment_names = wiki.attachment_names(page_name:)
23
+
24
+ if options[:json]
25
+ out.puts Formatter::JSON.new.format(attachment_names)
26
+ else
27
+ attachment_names.each {|name| out.puts name }
28
+ say("#{attachment_names.size} attachments found", out:, **options)
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ register "attachment list", Attachment::List
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ class CLI
5
+ module Commands
6
+ module Attachment
7
+ # Upload attachment to a page
8
+ class Put < Base
9
+ desc "Upload attachment to a page"
10
+
11
+ MAX_ATTACHMENT_SIZE = 512 * 1024 # 512 KiB
12
+ private_constant :MAX_ATTACHMENT_SIZE
13
+
14
+ argument :page_name, required: true, desc: "Page name"
15
+ argument :file_path, required: true, desc: "Local file path"
16
+ option :name, aliases: ["-n"], desc: "Attachment name (inferred from file path if omitted)"
17
+ option :force, aliases: ["-f"], type: :boolean, default: false, desc: "Overwrite existing attachment (non-atomic: deletes then uploads)"
18
+
19
+ # Execute the put command
20
+ #
21
+ # @param page_name [String] name of the page
22
+ # @param file_path [String] local file path to upload
23
+ # @param name [String, nil] optional attachment name (inferred from file_path if nil)
24
+ # @param force [Boolean] whether to overwrite existing attachment
25
+ # @param out [IO] output stream
26
+ # @param err [IO] error stream
27
+ # @return [void]
28
+ def call(page_name:, file_path:, name: nil, force: false, out: $stdout, err: $stderr, **)
29
+ wiki = create_wiki(out:, err:, **)
30
+
31
+ attachment_name = name || File.basename(file_path)
32
+ content = File.binread(file_path)
33
+
34
+ if content.bytesize > MAX_ATTACHMENT_SIZE
35
+ raise ArgumentError, "File size (#{content.bytesize} bytes) exceeds maximum allowed size (#{MAX_ATTACHMENT_SIZE} bytes / 512 KiB)"
36
+ end
37
+
38
+ begin
39
+ wiki.add_attachment(page_name:, attachment_name:, content:)
40
+ rescue ConflictError
41
+ raise ArgumentError, "Attachment '#{attachment_name}' already exists. Use --force to overwrite." unless force
42
+
43
+ wiki.delete_attachment(page_name:, attachment_name:)
44
+ retry
45
+ end
46
+
47
+ say("Attachment '#{attachment_name}' uploaded to page '#{page_name}'", out:, **)
48
+ end
49
+ end
50
+ end
51
+
52
+ register "attachment put", Attachment::Put
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ class CLI
5
+ module Commands
6
+ module Attachment
7
+ # Show attachment metadata
8
+ class Show < Base
9
+ desc "Show attachment metadata"
10
+
11
+ argument :page_name, required: true, desc: "Page name"
12
+ argument :file_name, required: true, desc: "Attachment file name"
13
+ option :json, aliases: ["-j"], type: :boolean, default: false, desc: "Output as JSON"
14
+
15
+ # Execute the show command
16
+ #
17
+ # @param page_name [String] name of the page
18
+ # @param file_name [String] name of the attachment file
19
+ # @param options [Hash] command options including wiki_id, auth, json, verbose, out, err
20
+ # @return [void]
21
+ def call(page_name:, file_name:, out: $stdout, err: $stderr, **options)
22
+ wiki = create_wiki(out:, err:, **options)
23
+ attachment = wiki.attachment(page_name:, attachment_name: file_name)
24
+
25
+ metadata = attachment.to_h.except(:content).transform_values {|v| v.is_a?(Time) ? v.iso8601 : v }
26
+
27
+ if options[:json]
28
+ out.puts Formatter::JSON.new.format(metadata)
29
+ else
30
+ out.puts "Page: #{metadata[:page_name]}"
31
+ out.puts "Name: #{metadata[:name]}"
32
+ out.puts "Size: #{metadata[:size]} bytes"
33
+ out.puts "Time: #{metadata[:time]}"
34
+ out.puts "Type: #{metadata[:type]}"
35
+ say("Attachment metadata retrieved", out:, **options)
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ register "attachment show", Attachment::Show
42
+ end
43
+ end
44
+ end