chocomint 1.0.2 → 1.1.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.
- checksums.yaml +4 -4
- data/README.md +27 -1
- data/THIRD_PARTY_LICENSES.md +1 -0
- data/lib/chocomint/edit_archive_handlers.rb +323 -0
- data/lib/chocomint/edit_handlers.rb +194 -55
- data/lib/chocomint/server.rb +5 -1
- data/lib/chocomint.rb +1 -1
- data/public/edit/edit.css +72 -3
- data/public/edit/edit.js +515 -118
- data/public/edit/index.html +9 -0
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 691a0248c4e919afa5640c7398b3f1b06221c397ac2c1e4c73fb56c044174246
|
|
4
|
+
data.tar.gz: 5371ad706155faff73120e8064f6629b7a776099e7c66fc5f07a89bfec932799
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75558e5f77a42aaaddaa2ffdfc5a509975cf7660ae5eaf346d9e5e24d45ce3873783f23e924b5516a6341db2f405e3ac1bd3688bf7677cc9fe0ec0074a5a352c
|
|
7
|
+
data.tar.gz: 0b43f17a5b4f916798db9107c3cef61d21f07da1299723a3feb1e31de43231f521bfc8e4bdfc560ff6dd1b65f9546819b61fb79e9263c90ef89796984cac48c9
|
data/README.md
CHANGED
|
@@ -191,11 +191,37 @@ ruby bin/chocomint-server
|
|
|
191
191
|
|----------------|------|
|
|
192
192
|
| `GET /edit` | エディタ画面 |
|
|
193
193
|
| `GET /edit/files` | `workspace/` のファイル一覧 (JSON) |
|
|
194
|
-
| `GET /edit/file?path=` | ファイル内容 (JSON) |
|
|
194
|
+
| `GET /edit/file?path=` | ファイル内容 (JSON)。画像/PDF/動画/音声は `media` 種別だけ返す |
|
|
195
|
+
| `GET /edit/raw?path=` | 生ファイル配信(メディアプレビュー用。`Range` 対応で動画のシークも可) |
|
|
196
|
+
| `GET /edit/archive?path=` | アーカイブの第一階層エントリ + メタデータ(zip / tar / tar.gz / bz2 等) |
|
|
195
197
|
| `POST /edit/save` | 人手編集の保存(`PathGuard` で `workspace/` 内に限定) |
|
|
196
198
|
| `POST /edit/chat` | 自然言語指示で AI 編集(`Planner#run`。`expectations` に指示を渡すため意味的検証が効く) |
|
|
197
199
|
| `GET /edit/assets/*` | Monaco / xterm.js のローカル同梱アセット |
|
|
198
200
|
|
|
201
|
+
### メディアプレビュー
|
|
202
|
+
|
|
203
|
+
テキストファイルは Monaco で編集し、以下のメディアはタブ内でプレビューする
|
|
204
|
+
(`workspace/` 配下のみ・`PathGuard` で保護):
|
|
205
|
+
|
|
206
|
+
- **画像**: PNG / JPEG / GIF / WebP / BMP / ICO / AVIF / TIFF / SVG(ベクター)
|
|
207
|
+
- **文書**: PDF(ブラウザ内蔵ビューアで表示)
|
|
208
|
+
- **動画**: MP4 / WebM / MKV / MOV / AVI / OGV(`<video>`。ブラウザが対応する形式のみ再生可)
|
|
209
|
+
- **音声**: MP3 / WAV / OGG / M4A / AAC / FLAC / Opus(`<audio>`)
|
|
210
|
+
|
|
211
|
+
動画・音声は `Range` リクエストに対応し、シークやストリーミング再生ができる。
|
|
212
|
+
ブラウザがコーデック非対応の形式(MKV の一部など)は再生できずメッセージを表示する。
|
|
213
|
+
|
|
214
|
+
### アーカイブプレビュー
|
|
215
|
+
|
|
216
|
+
zip / tar 系アーカイブをクリックすると、**第一階層(トップレベル)のエントリと
|
|
217
|
+
メタデータ**(エントリ数・展開後サイズ・圧縮サイズ)をタブ内に表示する。展開は
|
|
218
|
+
サーバー側でメモリ上に行い、ディスクには書き出さない。
|
|
219
|
+
|
|
220
|
+
- **対応**: `.zip` / `.tar` / `.tar.gz`(`.tgz`)/ `.gz` / `.tar.bz2`(`.tbz2`)/ `.bz2`
|
|
221
|
+
- 配下のエントリは親ディレクトリに畳み、その配下件数を添えて表示する。
|
|
222
|
+
- `.zip` は中央ディレクトリを自前パースするため rubyzip 非依存。`.bz2` は純Ruby
|
|
223
|
+
実装の `rbzip2` で展開する(ネイティブ `libbz2` 不要)。
|
|
224
|
+
|
|
199
225
|
### 追加依存
|
|
200
226
|
|
|
201
227
|
- `ffi` — Windows の擬似コンソール (ConPTY) を FFI で叩く(プリビルド gem、DevKit 不要)。
|
data/THIRD_PARTY_LICENSES.md
CHANGED
|
@@ -41,5 +41,6 @@ AI エディタ (`/edit`) をオフラインで動作させるため、以下を
|
|
|
41
41
|
| webrick | Ruby / BSD-2-Clause | https://github.com/ruby/webrick |
|
|
42
42
|
| ffi | BSD-3-Clause | https://github.com/ffi/ffi |
|
|
43
43
|
| websocket-driver | Apache-2.0 | https://github.com/faye/websocket-driver-ruby |
|
|
44
|
+
| rbzip2 | BSD-3-Clause | https://github.com/koraktor/rbzip2 |
|
|
44
45
|
|
|
45
46
|
上記はいずれも再配布・商用利用を許容する寛容なライセンスである。
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "zlib"
|
|
5
|
+
require "stringio"
|
|
6
|
+
require "rubygems/package"
|
|
7
|
+
|
|
8
|
+
module Chocomint
|
|
9
|
+
# /edit AI エディタのアーカイブプレビュー用ハンドラ (Server に include して使う)。
|
|
10
|
+
#
|
|
11
|
+
# 提供機能:
|
|
12
|
+
# GET /edit/archive?path=foo.zip アーカイブの第一階層エントリ + メタデータ (JSON)
|
|
13
|
+
#
|
|
14
|
+
# 対応形式: .zip / .tar / .tar.gz (.tgz) / .gz / .tar.bz2 (.tbz2) / .bz2
|
|
15
|
+
# いずれも展開はメモリ上で行い、ディスクには書き出さない。tar/zip はエントリを
|
|
16
|
+
# ストリーム走査し、第一階層 (トップレベル) だけに畳んで返す。
|
|
17
|
+
#
|
|
18
|
+
# 対応形式は拡張子でしか判定しない (中身の magic は見ない)。ファイルアクセスは
|
|
19
|
+
# すべて @path_guard 経由で workspace 内に制限する。
|
|
20
|
+
module EditArchiveHandlers
|
|
21
|
+
# 一覧をブラウザで扱える範囲に抑えるためのエントリ数上限 (走査自体の安全弁)。
|
|
22
|
+
# tar は全走査するため、巨大アーカイブでも到達したら打ち切って capped を立てる。
|
|
23
|
+
MAX_ARCHIVE_ENTRIES = 20_000
|
|
24
|
+
|
|
25
|
+
# bzip2 展開に許す入力サイズ上限 (純Ruby 実装は重いので極端に巨大な .bz2 は弾く)。
|
|
26
|
+
MAX_BZIP2_INPUT_BYTES = 64 * 1024 * 1024
|
|
27
|
+
|
|
28
|
+
def handle_edit_archive(req, res)
|
|
29
|
+
return edit_method_guard(res) unless req.request_method == "GET"
|
|
30
|
+
|
|
31
|
+
path = req.query["path"].to_s
|
|
32
|
+
return edit_json(res, 400, "error" => "path required") if path.empty?
|
|
33
|
+
|
|
34
|
+
abs = @path_guard.resolve(path)
|
|
35
|
+
return edit_json(res, 404, "error" => "no such file") unless File.file?(abs)
|
|
36
|
+
|
|
37
|
+
kind = archive_kind(abs)
|
|
38
|
+
return edit_json(res, 415, "error" => "unsupported archive format") unless kind
|
|
39
|
+
|
|
40
|
+
info = inspect_archive(abs, kind)
|
|
41
|
+
edit_json(res, 200, { "path" => path }.merge(info))
|
|
42
|
+
rescue Chocomint::PathAccessError => e
|
|
43
|
+
edit_json(res, 403, "error" => e.message)
|
|
44
|
+
rescue ArchiveError => e
|
|
45
|
+
# 壊れている/未対応の内部構造など。UI 向けにメッセージを返す (500 にはしない)。
|
|
46
|
+
edit_json(res, 422, "error" => e.message)
|
|
47
|
+
rescue Chocomint::Error => e
|
|
48
|
+
edit_json(res, 500, "error" => e.message)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# アーカイブ処理中の想定内エラー (壊れたデータ・展開失敗など)。
|
|
52
|
+
class ArchiveError < StandardError; end
|
|
53
|
+
|
|
54
|
+
# ---- 形式判定 -----------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
# 拡張子からアーカイブ形式を判定して種別文字列を返す (非対応は nil)。
|
|
57
|
+
# 種別: "zip" / "tar" / "tar.gz" / "tar.bz2" / "gz" / "bz2"
|
|
58
|
+
# 二重拡張子 (.tar.gz 等) を単一拡張子 (.gz) より優先する。
|
|
59
|
+
def archive_kind(abs)
|
|
60
|
+
lower = File.basename(abs).downcase
|
|
61
|
+
return "tar.gz" if lower.end_with?(".tar.gz")
|
|
62
|
+
return "tar.gz" if lower.end_with?(".tgz")
|
|
63
|
+
return "tar.bz2" if lower.end_with?(".tar.bz2")
|
|
64
|
+
return "tar.bz2" if lower.end_with?(".tbz2") || lower.end_with?(".tbz")
|
|
65
|
+
return "zip" if lower.end_with?(".zip")
|
|
66
|
+
return "tar" if lower.end_with?(".tar")
|
|
67
|
+
return "gz" if lower.end_with?(".gz")
|
|
68
|
+
return "bz2" if lower.end_with?(".bz2")
|
|
69
|
+
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# フロントがメディア/アーカイブを区別できるよう、handle_edit_file からも使う判定。
|
|
74
|
+
def archive?(abs)
|
|
75
|
+
!archive_kind(abs).nil?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# ---- 検査 (種別ごとにディスパッチ) --------------------------------------
|
|
79
|
+
|
|
80
|
+
def inspect_archive(abs, kind)
|
|
81
|
+
case kind
|
|
82
|
+
when "zip" then inspect_zip(abs)
|
|
83
|
+
when "tar" then inspect_tar(File.binread(abs))
|
|
84
|
+
when "tar.gz" then inspect_tar(gunzip(File.binread(abs)))
|
|
85
|
+
when "tar.bz2" then inspect_tar(bunzip2(abs))
|
|
86
|
+
when "gz" then inspect_single_compressed(abs, "gzip", strip_ext: ".gz")
|
|
87
|
+
when "bz2" then inspect_single_compressed(abs, "bzip2", strip_ext: ".bz2")
|
|
88
|
+
else
|
|
89
|
+
raise ArchiveError, "unsupported archive format"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# ---- tar の走査 ---------------------------------------------------------
|
|
94
|
+
#
|
|
95
|
+
# 展開済み tar バイト列を走査し、第一階層に畳んだ entries とメタデータを返す。
|
|
96
|
+
def inspect_tar(tar_bytes)
|
|
97
|
+
dirs = {} # トップレベルのディレクトリ名 => 配下エントリ数
|
|
98
|
+
files = [] # トップレベルの直下ファイル { name, size }
|
|
99
|
+
total = 0
|
|
100
|
+
total_size = 0
|
|
101
|
+
capped = false
|
|
102
|
+
|
|
103
|
+
Gem::Package::TarReader.new(StringIO.new(tar_bytes)) do |reader|
|
|
104
|
+
reader.each do |entry|
|
|
105
|
+
name = entry.full_name.to_s
|
|
106
|
+
next if name.empty? || name == "./"
|
|
107
|
+
|
|
108
|
+
total += 1
|
|
109
|
+
if total > MAX_ARCHIVE_ENTRIES
|
|
110
|
+
capped = true
|
|
111
|
+
break
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
rel = normalize_entry_name(name)
|
|
115
|
+
next if rel.empty?
|
|
116
|
+
|
|
117
|
+
size = entry.header.size.to_i
|
|
118
|
+
total_size += size unless entry.directory?
|
|
119
|
+
accumulate_top_level(rel, entry.directory?, size, dirs, files)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
build_result(dirs, files, total, total_size, capped)
|
|
124
|
+
rescue Gem::Package::TarInvalidError, Zlib::GzipFile::Error => e
|
|
125
|
+
raise ArchiveError, "tar を読み取れませんでした: #{e.message}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# ---- zip の走査 (中央ディレクトリを自前パース) --------------------------
|
|
129
|
+
#
|
|
130
|
+
# 標準ライブラリだけで済ませるため rubyzip には依存せず、ZIP の End Of Central
|
|
131
|
+
# Directory (EOCD) → Central Directory を辿ってエントリ名とサイズを読む。
|
|
132
|
+
# 実データ (ローカルファイルヘッダ) は読まないので大きな zip でも軽い。
|
|
133
|
+
def inspect_zip(abs)
|
|
134
|
+
raw = File.binread(abs)
|
|
135
|
+
cd = zip_central_directory(raw)
|
|
136
|
+
|
|
137
|
+
dirs = {}
|
|
138
|
+
files = []
|
|
139
|
+
total = 0
|
|
140
|
+
total_size = 0
|
|
141
|
+
capped = false
|
|
142
|
+
|
|
143
|
+
cd.each do |ent|
|
|
144
|
+
total += 1
|
|
145
|
+
if total > MAX_ARCHIVE_ENTRIES
|
|
146
|
+
capped = true
|
|
147
|
+
break
|
|
148
|
+
end
|
|
149
|
+
rel = normalize_entry_name(ent[:name])
|
|
150
|
+
next if rel.empty?
|
|
151
|
+
|
|
152
|
+
total_size += ent[:size] unless ent[:dir]
|
|
153
|
+
accumulate_top_level(rel, ent[:dir], ent[:size], dirs, files)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
build_result(dirs, files, total, total_size, capped)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# ZIP の中央ディレクトリを読み、各エントリの { name, size, dir } を返す。
|
|
160
|
+
def zip_central_directory(raw)
|
|
161
|
+
eocd = find_eocd(raw)
|
|
162
|
+
raise ArchiveError, "zip の終端レコードが見つかりません" unless eocd
|
|
163
|
+
|
|
164
|
+
count = raw[eocd + 10, 2].unpack1("v")
|
|
165
|
+
offset = raw[eocd + 16, 4].unpack1("V")
|
|
166
|
+
entries = []
|
|
167
|
+
pos = offset
|
|
168
|
+
|
|
169
|
+
count.times do
|
|
170
|
+
break if pos + 46 > raw.bytesize
|
|
171
|
+
sig = raw[pos, 4]
|
|
172
|
+
break unless sig == "PK\x01\x02".b # Central Directory File Header
|
|
173
|
+
|
|
174
|
+
usize = raw[pos + 24, 4].unpack1("V") # uncompressed size
|
|
175
|
+
name_len = raw[pos + 28, 2].unpack1("v")
|
|
176
|
+
extra_len = raw[pos + 30, 2].unpack1("v")
|
|
177
|
+
comment_len = raw[pos + 32, 2].unpack1("v")
|
|
178
|
+
name = raw[pos + 46, name_len].to_s
|
|
179
|
+
name = zip_decode_name(name)
|
|
180
|
+
entries << { name: name, size: usize, dir: name.end_with?("/") }
|
|
181
|
+
pos += 46 + name_len + extra_len + comment_len
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
entries
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# EOCD (End Of Central Directory, シグネチャ PK\x05\x06) を末尾から探す。
|
|
188
|
+
# ZIP コメントは最大 65535 バイトなので、末尾 (22 + 65535) だけ後方走査すれば十分。
|
|
189
|
+
def find_eocd(raw)
|
|
190
|
+
sig = "PK\x05\x06".b
|
|
191
|
+
min = [raw.bytesize - (22 + 0xFFFF), 0].max
|
|
192
|
+
i = raw.bytesize - 22
|
|
193
|
+
while i >= min
|
|
194
|
+
return i if raw[i, 4] == sig
|
|
195
|
+
|
|
196
|
+
i -= 1
|
|
197
|
+
end
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# zip のエントリ名を文字列に整える。UTF-8 として妥当ならそれ、駄目なら置換して壊さない。
|
|
202
|
+
def zip_decode_name(name)
|
|
203
|
+
s = name.dup.force_encoding("UTF-8")
|
|
204
|
+
s.valid_encoding? ? s : s.scrub("?")
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# ---- 単体圧縮ファイル (.gz / .bz2、tar でない) --------------------------
|
|
208
|
+
#
|
|
209
|
+
# tar を含まない単なる圧縮ファイルは、展開後の 1 ファイルとして扱う。
|
|
210
|
+
# 中身の全展開は避け、メタデータ (圧縮/展開サイズ・推定名) だけを返す。
|
|
211
|
+
def inspect_single_compressed(abs, algorithm, strip_ext:)
|
|
212
|
+
compressed_size = File.size(abs)
|
|
213
|
+
inner_name = File.basename(abs)
|
|
214
|
+
inner_name = inner_name[0...-strip_ext.length] if inner_name.downcase.end_with?(strip_ext)
|
|
215
|
+
|
|
216
|
+
uncompressed_size =
|
|
217
|
+
case algorithm
|
|
218
|
+
when "gzip" then gzip_uncompressed_size(abs)
|
|
219
|
+
when "bzip2" then bunzip2(abs).bytesize
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
{
|
|
223
|
+
"archive" => algorithm == "gzip" ? "gz" : "bz2",
|
|
224
|
+
"single" => true,
|
|
225
|
+
"entries" => [{ "name" => inner_name, "dir" => false, "size" => uncompressed_size }],
|
|
226
|
+
"total_entries" => 1,
|
|
227
|
+
"total_size" => uncompressed_size,
|
|
228
|
+
"compressed_size" => compressed_size,
|
|
229
|
+
"capped" => false
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# ---- 展開ヘルパ ---------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
def gunzip(bytes)
|
|
236
|
+
Zlib::GzipReader.new(StringIO.new(bytes)).read
|
|
237
|
+
rescue Zlib::GzipFile::Error => e
|
|
238
|
+
raise ArchiveError, "gzip を展開できませんでした: #{e.message}"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# gzip 末尾 4 バイトの ISIZE (展開後サイズ mod 2^32)。4GB 未満の実用ケース向け。
|
|
242
|
+
def gzip_uncompressed_size(abs)
|
|
243
|
+
tail = File.open(abs, "rb") { |f| f.seek(-4, IO::SEEK_END); f.read(4) }
|
|
244
|
+
tail ? tail.unpack1("V") : nil
|
|
245
|
+
rescue SystemCallError
|
|
246
|
+
nil
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# bzip2 を純Ruby (rbzip2) で展開する。ネイティブ libbz2 は不要。
|
|
250
|
+
def bunzip2(abs)
|
|
251
|
+
require "rbzip2"
|
|
252
|
+
raw = File.binread(abs)
|
|
253
|
+
if raw.bytesize > MAX_BZIP2_INPUT_BYTES
|
|
254
|
+
raise ArchiveError, "bzip2 ファイルが大きすぎます (上限 #{MAX_BZIP2_INPUT_BYTES / (1024 * 1024)}MB)"
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
RBzip2.default_adapter::Decompressor.new(StringIO.new(raw)).read
|
|
258
|
+
rescue LoadError
|
|
259
|
+
raise ArchiveError, "bzip2 の展開には rbzip2 gem が必要です"
|
|
260
|
+
rescue ArchiveError
|
|
261
|
+
raise
|
|
262
|
+
rescue StandardError => e
|
|
263
|
+
raise ArchiveError, "bzip2 を展開できませんでした: #{e.message}"
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# ---- 集計の共通処理 -----------------------------------------------------
|
|
267
|
+
|
|
268
|
+
# エントリ名を正規化する ("./" 前置や重複スラッシュを除く。先頭は相対に統一)。
|
|
269
|
+
def normalize_entry_name(name)
|
|
270
|
+
s = name.tr("\\", "/")
|
|
271
|
+
s = s.sub(%r{\A\./}, "")
|
|
272
|
+
s = s.sub(%r{\A/+}, "")
|
|
273
|
+
s.gsub(%r{/+}, "/")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# 正規化済みエントリを「第一階層」に畳んで dirs / files に足し込む。
|
|
277
|
+
# "src/main.rb" → トップレベル dir "src" を登録し配下件数 +1
|
|
278
|
+
# "README.md" → トップレベル直下 file "README.md"
|
|
279
|
+
# "src/" → dir "src" を登録するだけ (ディレクトリ自身なので件数は増やさない)
|
|
280
|
+
# "emptydir/" → dir "emptydir" を件数 0 で登録 (空ディレクトリ)
|
|
281
|
+
def accumulate_top_level(rel, is_dir, size, dirs, files)
|
|
282
|
+
# 末尾スラッシュはディレクトリ自身のエントリ。区切りとしての "/" と区別する。
|
|
283
|
+
trailing_dir = rel.end_with?("/")
|
|
284
|
+
trimmed = trailing_dir ? rel.chomp("/") : rel
|
|
285
|
+
slash = trimmed.index("/")
|
|
286
|
+
|
|
287
|
+
if slash.nil?
|
|
288
|
+
# トップレベル直下のエントリ (ファイル or ディレクトリ自身)。
|
|
289
|
+
if is_dir || trailing_dir
|
|
290
|
+
dirs[trimmed] ||= 0 # 件数は増やさない (自身の登録だけ)。
|
|
291
|
+
else
|
|
292
|
+
files << { name: trimmed, size: size }
|
|
293
|
+
end
|
|
294
|
+
else
|
|
295
|
+
# 配下エントリ → トップレベルのディレクトリに畳んで件数を +1。
|
|
296
|
+
top = trimmed[0...slash]
|
|
297
|
+
dirs[top] ||= 0
|
|
298
|
+
dirs[top] += 1
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# dirs / files から JSON 用の結果ハッシュを組み立てる。
|
|
303
|
+
# entries はディレクトリ→ファイルの順、各名前順 (一覧の並びに揃える)。
|
|
304
|
+
def build_result(dirs, files, total, total_size, capped)
|
|
305
|
+
dir_entries = dirs.keys.sort.map do |name|
|
|
306
|
+
{ "name" => name, "dir" => true, "child_count" => dirs[name] }
|
|
307
|
+
end
|
|
308
|
+
# 同名ファイルの重複は稀だが、名前順で安定させる。
|
|
309
|
+
file_entries = files.uniq { |f| f[:name] }
|
|
310
|
+
.sort_by { |f| f[:name] }
|
|
311
|
+
.map { |f| { "name" => f[:name], "dir" => false, "size" => f[:size] } }
|
|
312
|
+
|
|
313
|
+
{
|
|
314
|
+
"archive" => "tar_or_zip",
|
|
315
|
+
"single" => false,
|
|
316
|
+
"entries" => dir_entries + file_entries,
|
|
317
|
+
"total_entries" => total,
|
|
318
|
+
"total_size" => total_size,
|
|
319
|
+
"capped" => capped
|
|
320
|
+
}
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|