wikiwiki 0.5.0 → 0.6.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/README.ja.md +93 -3
  4. data/README.md +93 -3
  5. data/exe/wikiwiki +8 -0
  6. data/lib/wikiwiki/api.rb +38 -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 +48 -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 +60 -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/get.rb +46 -0
  17. data/lib/wikiwiki/cli/commands/page/list.rb +36 -0
  18. data/lib/wikiwiki/cli/commands/page/put.rb +40 -0
  19. data/lib/wikiwiki/cli/commands/page/show.rb +44 -0
  20. data/lib/wikiwiki/cli/formatter/json.rb +18 -0
  21. data/lib/wikiwiki/cli.rb +51 -0
  22. data/lib/wikiwiki/rate_limiter.rb +4 -12
  23. data/lib/wikiwiki/sliding_window.rb +1 -3
  24. data/lib/wikiwiki/version.rb +1 -1
  25. data/lib/wikiwiki/wiki.rb +7 -2
  26. data/lib/wikiwiki.rb +1 -1
  27. data/sig/dry/cli.rbs +9 -0
  28. data/sig/wikiwiki/api.rbs +5 -3
  29. data/sig/wikiwiki/auth/token.rbs +9 -0
  30. data/sig/wikiwiki/auth.rbs +2 -0
  31. data/sig/wikiwiki/cli/commands/attachment.rbs +96 -0
  32. data/sig/wikiwiki/cli/commands/auth.rbs +9 -0
  33. data/sig/wikiwiki/cli/commands/base.rbs +27 -0
  34. data/sig/wikiwiki/cli/commands/page.rbs +67 -0
  35. data/sig/wikiwiki/cli/formatter/json.rbs +9 -0
  36. data/sig/wikiwiki/cli.rbs +11 -0
  37. data/sig/wikiwiki/wiki.rbs +3 -1
  38. metadata +59 -8
  39. data/LICENSE.txt +0 -21
  40. data/mise.toml +0 -6
  41. 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: bc13d22d549b9398dc6573d2731c7c1169fe70a1626f80d9f9b650116deb162c
4
+ data.tar.gz: 1495f98f015f7494f7355a76f377f42d082a72e92a4679438b2d8822f6294f91
5
5
  SHA512:
6
- metadata.gz: 9bae7372d114df3f32f4f60d3ae1b4664ff3581c30ba0215e5d705de733063b2d12faaa92c557439f29b1e9b9707e9c3993400e738b672125365b7be24d401c1
7
- data.tar.gz: f6e397e07040ba97328609f437a30a29be059c2df7c7051d78def34c204f6d5bf6005e618014893ffddcb36e20cc41db414eb1187435776a60217b2177c960ee
6
+ metadata.gz: ad6ac01ea174e040f151f74d04504314bb4885abb0e66cc915debddee70f5d6ad3de402def7c693ae52c3ba8f1f3dd9fe8497befbb3c736ae5e79d86e79ed56a
7
+ data.tar.gz: 0351abf501fed5484cc1b9606610d3ed3fbc789064a11384470ee7c7abd07115c27f315568d42a4ab4ead63f5be05473bef832cfdc350afdfa04c90e76774a68
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.6.0] - 2025-11-02
4
+
5
+ ### Added
6
+
7
+ - Pre-obtained token reuse support
8
+ - `Auth.token(token:)` for authentication with pre-obtained JWT tokens
9
+ - `wikiwiki auth` command to obtain authentication tokens
10
+ - `Wiki#token` method to retrieve the current authentication token
11
+ - `--token` option and `WIKIWIKI_TOKEN` environment variable support for all commands
12
+ - JWT token expiration validation with AuthenticationError for expired tokens
13
+ - Command-line interface (`wikiwiki` command) for all API operations
14
+ - Page commands: `list`, `show`, `get`, `put`
15
+ - Attachment commands: `list`, `show`, `get`, `put`, `delete`
16
+ - Authentication command: `auth`
17
+ - Environment variable support for credentials (WIKIWIKI_WIKI_ID, WIKIWIKI_TOKEN, WIKIWIKI_PASSWORD, WIKIWIKI_API_KEY_ID, WIKIWIKI_SECRET)
18
+ - JSON output option (`--json`) for automation
19
+ - Verbose (`--verbose`) and debug (`--debug`) modes
20
+ - File overwrite protection with `--force` flag
21
+ - Attachment size limit validation (512 KiB) for uploads
22
+
3
23
  ## [0.5.0] - 2025-10-31
4
24
 
5
25
  ### 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,91 @@ 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 attachment list FrontPage
92
+
93
+ # 添付ファイルのメタデータ表示
94
+ wikiwiki attachment show FrontPage logo.png
95
+
96
+ # 添付ファイルをダウンロード
97
+ wikiwiki attachment get FrontPage logo.png
98
+ wikiwiki attachment get FrontPage logo.png --directory downloads/
99
+
100
+ # 添付ファイルをアップロード(最大512 KiB)
101
+ wikiwiki attachment put FrontPage image.png
102
+ wikiwiki attachment put FrontPage local.png --name remote.png
103
+
104
+ # 添付ファイルを削除
105
+ wikiwiki attachment delete FrontPage logo.png
106
+
107
+ # 既存のファイル/添付ファイルを上書きするには --force を使用
108
+ wikiwiki page get FrontPage existing.txt --force
109
+ wikiwiki attachment put FrontPage logo.png --force
110
+
111
+ # 注意: --force による添付ファイルの上書きはアトミックではありません。
112
+ # 既存の添付ファイルを削除してから新しいファイルをアップロードします。
113
+ # アップロードに失敗した場合、添付ファイルは失われます。
114
+
115
+ # コマンドラインで認証情報を指定(環境変数より優先)
116
+ wikiwiki --wiki-id=your-wiki-id --password=your-password page list
117
+ wikiwiki --wiki-id=your-wiki-id --api-key-id=id --secret=secret page list
118
+ wikiwiki --wiki-id=your-wiki-id --token=eyJ... page list
119
+
120
+ # 自動化のためのJSON出力
121
+ wikiwiki page list --json
122
+ wikiwiki attachment show FrontPage logo.png --json
123
+
124
+ # 詳細モードとデバッグモード
125
+ wikiwiki page list --verbose
126
+ wikiwiki page list --debug
127
+ ```
128
+
129
+ ### Rubyライブラリ
130
+
131
+ ライブラリを使用する基本的な例:
50
132
 
51
133
  ```ruby
52
134
  require "wikiwiki"
@@ -55,6 +137,14 @@ require "wikiwiki"
55
137
  auth = Wikiwiki::Auth.password(password: "admin_password")
56
138
  wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
57
139
 
140
+ # 認証トークンを取得して再利用
141
+ token = wiki.token
142
+ # トークンを保存して後で使用...
143
+
144
+ # 後で保存したトークンを使用
145
+ auth = Wikiwiki::Auth.token(token: token)
146
+ wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
147
+
58
148
  # すべてのページ名の一覧を取得
59
149
  page_names = wiki.page_names
60
150
  # => ["FrontPage", "SideBar", ...]
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,91 @@ 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
+ # List attachments
91
+ wikiwiki attachment list FrontPage
92
+
93
+ # Show attachment metadata
94
+ wikiwiki attachment show FrontPage logo.png
95
+
96
+ # Download attachment
97
+ wikiwiki attachment get FrontPage logo.png
98
+ wikiwiki attachment get FrontPage logo.png --directory downloads/
99
+
100
+ # Upload attachment (max 512 KiB)
101
+ wikiwiki attachment put FrontPage image.png
102
+ wikiwiki attachment put FrontPage local.png --name remote.png
103
+
104
+ # Delete attachment
105
+ wikiwiki attachment delete FrontPage logo.png
106
+
107
+ # Use --force to overwrite existing files/attachments
108
+ wikiwiki page get FrontPage existing.txt --force
109
+ wikiwiki attachment put FrontPage logo.png --force
110
+
111
+ # Note: Attachment overwrite with --force is not atomic.
112
+ # The existing attachment is deleted before uploading the new one.
113
+ # If the upload fails, the attachment will be lost.
114
+
115
+ # Authentication via command line (overrides environment variables)
116
+ wikiwiki --wiki-id=your-wiki-id --password=your-password page list
117
+ wikiwiki --wiki-id=your-wiki-id --api-key-id=id --secret=secret page list
118
+ wikiwiki --wiki-id=your-wiki-id --token=eyJ... page list
119
+
120
+ # JSON output for automation
121
+ wikiwiki page list --json
122
+ wikiwiki attachment show FrontPage logo.png --json
123
+
124
+ # Verbose and debug modes
125
+ wikiwiki page list --verbose
126
+ wikiwiki page list --debug
127
+ ```
128
+
129
+ ### Ruby Library
130
+
131
+ Basic example using the library:
50
132
 
51
133
  ```ruby
52
134
  require "wikiwiki"
@@ -55,6 +137,14 @@ require "wikiwiki"
55
137
  auth = Wikiwiki::Auth.password(password: "admin_password")
56
138
  wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
57
139
 
140
+ # Obtain and reuse authentication token
141
+ token = wiki.token
142
+ # Save token for later use...
143
+
144
+ # Later, use the saved token
145
+ auth = Wikiwiki::Auth.token(token: token)
146
+ wiki = Wikiwiki::Wiki.new(wiki_id: "your-wiki-id", auth:)
147
+
58
148
  # List all page names
59
149
  page_names = wiki.page_names
60
150
  # => ["FrontPage", "SideBar", ...]
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
@@ -123,14 +125,44 @@ module Wikiwiki
123
125
 
124
126
  # Authenticate with the Wikiwiki API
125
127
  #
126
- # @param auth [Auth::Password, Auth::ApiKey] authentication credentials
128
+ # @param auth [Auth::Password, Auth::ApiKey, Auth::Token] authentication credentials
127
129
  # @return [String] JWT token
128
130
  # @raise [Error] if authentication fails
129
131
  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"]
132
+ token = if auth.is_a?(Auth::Token)
133
+ auth.token
134
+ else
135
+ uri = BASE_URL + "/#{wiki_id}/auth"
136
+ response = request(:post, uri, body: auth.to_h, authenticate: false)
137
+ data = parse_json_response(response)
138
+ data["token"]
139
+ end
140
+
141
+ validate_token_expiry(token) if auth.is_a?(Auth::Token)
142
+ token
143
+ end
144
+
145
+ # Validate JWT token expiry
146
+ #
147
+ # @param token [String] JWT token
148
+ # @return [void]
149
+ # @raise [AuthenticationError] if token has expired
150
+ private def validate_token_expiry(token)
151
+ payload, = JWT.decode(token, nil, false)
152
+ exp = payload["exp"]
153
+
154
+ if exp
155
+ exp_time = Time.at(exp)
156
+ if Time.now >= exp_time
157
+ raise AuthenticationError, "Token has expired at #{exp_time.iso8601}"
158
+ end
159
+
160
+ logger.debug("[#{wiki_id}] Token expires at: #{exp_time.iso8601}")
161
+ else
162
+ logger.debug("[#{wiki_id}] Token has no expiration")
163
+ end
164
+ rescue JWT::DecodeError => e
165
+ logger.debug("[#{wiki_id}] Failed to decode token: #{e.message}")
134
166
  end
135
167
 
136
168
  # Parse JSON response
@@ -217,6 +249,6 @@ module Wikiwiki
217
249
  end
218
250
  end
219
251
 
220
- private attr_reader :wiki_id, :token
252
+ private attr_reader :wiki_id
221
253
  end
222
254
  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,48 @@
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
+ # Check if page exists first
25
+ unless page_exists?(wiki, page_name:)
26
+ raise ArgumentError, "Page '#{page_name}' does not exist"
27
+ end
28
+
29
+ # Check if attachment exists
30
+ unless attachment_exists?(wiki, page_name:, attachment_name: file_name)
31
+ raise ArgumentError, "Attachment '#{file_name}' does not exist on page '#{page_name}'"
32
+ end
33
+
34
+ wiki.delete_attachment(page_name:, attachment_name: file_name)
35
+
36
+ say("Attachment '#{file_name}' deleted from page '#{page_name}'", out:, **)
37
+ end
38
+
39
+ private def page_exists?(wiki, page_name:) = wiki.page_names.include?(page_name)
40
+
41
+ private def attachment_exists?(wiki, page_name:, attachment_name:) = wiki.attachment_names(page_name:).include?(attachment_name)
42
+ end
43
+ end
44
+
45
+ register "attachment delete", Attachment::Delete
46
+ end
47
+ end
48
+ 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,60 @@
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
+ if attachment_exists?(wiki, page_name:, attachment_name:)
39
+ raise ArgumentError, "Attachment '#{attachment_name}' already exists. Use --force to overwrite." unless force
40
+
41
+ begin
42
+ wiki.delete_attachment(page_name:, attachment_name:)
43
+ rescue ResourceNotFoundError
44
+ # Already deleted by another process, continue
45
+ end
46
+ end
47
+
48
+ wiki.add_attachment(page_name:, attachment_name:, content:)
49
+
50
+ say("Attachment '#{attachment_name}' uploaded to page '#{page_name}'", out:, **)
51
+ end
52
+
53
+ private def attachment_exists?(wiki, page_name:, attachment_name:) = wiki.attachment_names(page_name:).include?(attachment_name)
54
+ end
55
+ end
56
+
57
+ register "attachment put", Attachment::Put
58
+ end
59
+ end
60
+ 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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wikiwiki
4
+ class CLI
5
+ module Commands
6
+ # Authenticate and output JWT token
7
+ class Auth < Base
8
+ desc "Authenticate and output JWT token"
9
+
10
+ option :json, aliases: ["-j"], type: :boolean, default: false, desc: "Output as JSON"
11
+
12
+ # Execute the auth command
13
+ #
14
+ # @param options [Hash] command options including wiki_id, auth credentials, json, verbose, out, err
15
+ # @return [void]
16
+ def call(out: $stdout, err: $stderr, **options)
17
+ wiki = create_wiki(out:, err:, **options)
18
+ token = wiki.token
19
+
20
+ if options[:json]
21
+ out.puts Formatter::JSON.new.format({"token" => token})
22
+ else
23
+ out.puts token
24
+ say("Authentication successful", out:, **options)
25
+ end
26
+ end
27
+ end
28
+
29
+ register "auth", Auth
30
+ end
31
+ end
32
+ end