-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
576 lines (286 loc) · 378 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<follow_challenge>
<feedId>69266541190951936</feedId>
<userId>68553932502853632</userId>
</follow_challenge>
<title>小麦</title>
<subtitle>人生的大起大落来得太突然,搞得我直想尿尿...</subtitle>
<link href="https://xmaihh.github.io/blog/atom.xml" rel="self"/>
<link href="https://pubsubhubbub.appspot.com/" rel="hub"/>
<link href="https://xmaihh.github.io/blog/"/>
<updated>2024-10-17T01:49:01.000Z</updated>
<id>https://xmaihh.github.io/blog/</id>
<author>
<name>xmaihh</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>Hexo博客启用 WebSub</title>
<link href="https://xmaihh.github.io/blog/2024/10/17/hexo-bo-ke-qi-yong-websub/"/>
<id>https://xmaihh.github.io/blog/2024/10/17/hexo-bo-ke-qi-yong-websub/</id>
<published>2024-10-17T01:49:01.000Z</published>
<updated>2024-10-17T01:49:01.000Z</updated>
<content type="html"><![CDATA[<p><a href="https://pubsubhubbub.appspot.com/">WebSub(前身为 PubSubHubbub)</a>是一种能够实时通知内容更新的协议。它基于发布者/订阅者模式,即发布者发布内容更新,订阅者接收这些更新。</p><ul><li><p>发布者 = 博客网站</p></li><li><p>订阅者 = 使用 Feed 解析器的读者</p></li><li><p>中转 = WebSub</p></li></ul><p>WebSub 的主要目的是提供实时的变化通知,改善了客户端以任意时间间隔定期轮询 feed 服务器的典型情况。这样,WebSub 就能提供 HTTP 推送通知,而不需要客户端花费资源来轮询变化。</p><p>使用 WebSub 的好处:</p><ul><li>使用 RSS 阅读器的读者能够更快收到新文章</li><li>减少 feed 解析器向网页服务器发送的请求数量,节省带宽。 不使用 WebSub 的话,feed 解析器为了保持及时更新,会不断地(e.g. 每半小时)向网页服务器请求下载一次 Feed XML 文件,以此对比变化。让服务器压力增大</li><li>或许能加快 Google 录取博客新文章到索引的速度</li></ul><p>我使用<a href="https://github.com/hexojs/hexo-generator-feed">hexo-generator-feed</a>插件来生成 RSS 订阅源,详细配置选项请参考<a href="https://github.com/hexojs/hexo-generator-feed#options">官方文档</a></p><h1 id="修改配置文件"><a href="#修改配置文件" class="headerlink" title="修改配置文件"></a>修改配置文件</h1><p>修改根目录下的<code>_config.yml</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">feed:</span></span><br><span class="line"> <span class="attr">hub:</span> <span class="string">https://pubsubhubbub.appspot.com</span></span><br></pre></td></tr></table></figure><p><code>hexo g</code> 生成博客,我的 <code>atom.xml</code> 文件中多了一行代码:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">link</span> <span class="attr">href</span>=<span class="string">"https://pubsubhubbub.appspot.com/"</span> <span class="attr">rel</span>=<span class="string">"hub"</span>/></span></span><br></pre></td></tr></table></figure><p>本地运行看到的效果。</p><p><img src="https://s2.loli.net/2024/10/17/quDQjkJ5FKdt3sy.png"></p><h1 id="设置WebHook"><a href="#设置WebHook" class="headerlink" title="设置WebHook"></a>设置WebHook</h1><p>在GitHub Pages仓库,<code>Settings</code> -> <code>Webhooks</code> -> <code>Add webhook</code></p><p><img src="https://s2.loli.net/2024/10/17/Q3ulWUCEwAKY6ZM.png"></p><ul><li><p><strong>Payload URL</strong> 填入格式:<a href="https://pubsubhubbub.appspot.com/publish?hub.mode=publish&hub.url=%E5%8D%9A%E5%AE%A2">https://pubsubhubbub.appspot.com/publish?hub.mode=publish&hub.url=博客</a> RSS URL</p><p>e.g. <a href="https://pubsubhubbub.appspot.com/publish?hub.mode=publish&hub.url=https://xmaihh.github.io/blog/atom.xml">https://pubsubhubbub.appspot.com/publish?hub.mode=publish&hub.url=https://xmaihh.github.io/blog/atom.xml</a></p></li><li><p><strong>Content type</strong> 选 <code>application/x-www-form-urlencoded</code></p></li><li><p><strong>触发 Webhook</strong> 的方式:<code>Page builds</code>,这样每次站点更新后就会触发 Webhook。</p></li></ul><p>保存设置后,手动激活 webhook 进行测试,得到结果 <code>204</code> 意味设置成功。</p><p><img src="https://s2.loli.net/2024/10/17/163wyG75mTuHbYB.png"></p><h1 id="验证WebSub是否设置成功"><a href="#验证WebSub是否设置成功" class="headerlink" title="验证WebSub是否设置成功"></a>验证WebSub是否设置成功</h1><p>使用谷歌提供的 <a href="https://pubsubhubbub.appspot.com/publish">PubSubHubbub 工具</a> ,查看WebSub是否设置成功。</p><p>填入博客 RSS URL,然后 Get Info。</p><p><img src="https://s2.loli.net/2024/10/17/MVxfiuLDbeTv5sA.png"></p><p>查看上次更新时间Last successful fetch,已经支持实时推送了。</p><p><img src="https://s2.loli.net/2024/10/17/1NgoOHBFIki8zaL.png"></p><h1 id="Refence"><a href="#Refence" class="headerlink" title="Refence"></a>Refence</h1><p><a href="https://blog.haysc.tech/hexo-feed-setup/">提升 RSS 体验:Hexo 博客 Feed 指北</a></p><p><a href="https://sekibetu.com/rss01.html">给 Hexo 博客加上 PubSubHubbub 协议实现 RSS 实时推送</a></p>]]></content>
<summary type="html"><p><a href="https://pubsubhubbub.appspot.com/">WebSub(前身为 PubSubHubbub)</a>是一种能够实时通知内容更新的协议。它基于发布者&#x2F;订阅者模式,即发布者发布内容更新,订阅者接收这些更新。</p>
<ul></summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Hexo" scheme="https://xmaihh.github.io/blog/tags/Hexo/"/>
</entry>
<entry>
<title>让Follow认证我的Hexo博客订阅源</title>
<link href="https://xmaihh.github.io/blog/2024/10/16/rang-follow-ren-zheng-wo-de-hexo-bo-ke-ding-yue-yuan/"/>
<id>https://xmaihh.github.io/blog/2024/10/16/rang-follow-ren-zheng-wo-de-hexo-bo-ke-ding-yue-yuan/</id>
<published>2024-10-16T07:48:14.000Z</published>
<updated>2024-10-16T07:48:14.000Z</updated>
<content type="html"><![CDATA[<p>首先在Follow添加订阅源 :<a href="https://xmaihh.github.io/blog/atom.xml">https://xmaihh.github.io/blog/atom.xml</a></p><p><img src="https://s2.loli.net/2024/10/16/rL8P9aulcNBZ7dA.png"></p><p>添加订阅源之后,在Follow上获得需要认证的代码</p><p><img src="https://s2.loli.net/2024/10/16/dB8fIVkPwWLpA71.png"></p><p><img src="https://s2.loli.net/2024/10/16/dKyHLBUxYewf69t.png"></p><p>我使用<a href="https://github.com/hexojs/hexo-generator-feed">hexo-generator-feed</a>插件来生成 RSS订阅源,详细配置选项请参考<a href="https://github.com/hexojs/hexo-generator-feed#options">官方文档</a></p><p>修改根目录下的<code>_config.yml</code>:</p><p><img src="https://s2.loli.net/2024/10/16/SOHYc3mEtP6xWJ9.png"></p><p>原理是:修改自定义模板,使用该模板文件将用于生成 feed xml 文件。</p><p>所以将Follow认证的代码插入:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">follow_challenge</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">feedId</span>></span>69266541190951936<span class="tag"></<span class="name">feedId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">userId</span>></span>68553932502853632<span class="tag"></<span class="name">userId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">follow_challenge</span>></span> </span><br></pre></td></tr></table></figure><p><code>_custom_atom.xml</code>完整内容如下:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"utf-8"</span>?></span></span><br><span class="line"><span class="tag"><<span class="name">feed</span> <span class="attr">xmlns</span>=<span class="string">"http://www.w3.org/2005/Atom"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">follow_challenge</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">feedId</span>></span>69266541190951936<span class="tag"></<span class="name">feedId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">userId</span>></span>68553932502853632<span class="tag"></<span class="name">userId</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">follow_challenge</span>></span> </span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>{{ config.title }}<span class="tag"></<span class="name">title</span>></span></span><br><span class="line"> {% if icon %}<span class="tag"><<span class="name">icon</span>></span>{{ icon }}<span class="tag"></<span class="name">icon</span>></span>{% endif %}</span><br><span class="line"> {% if config.subtitle %}<span class="tag"><<span class="name">subtitle</span>></span>{{ config.subtitle }}<span class="tag"></<span class="name">subtitle</span>></span>{% endif %}</span><br><span class="line"> <span class="tag"><<span class="name">link</span> <span class="attr">href</span>=<span class="string">"{{ feed_url | uriencode }}"</span> <span class="attr">rel</span>=<span class="string">"self"</span>/></span></span><br><span class="line"> {% if config.feed.hub %}<span class="tag"><<span class="name">link</span> <span class="attr">href</span>=<span class="string">"{{ config.feed.hub | uriencode }}"</span> <span class="attr">rel</span>=<span class="string">"hub"</span>/></span>{% endif %}</span><br><span class="line"> <span class="tag"><<span class="name">link</span> <span class="attr">href</span>=<span class="string">"{{ url | uriencode }}"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">updated</span>></span>{% if posts.first().updated %}{{ posts.first().updated.toISOString() }}{% else %}{{ posts.first().date.toISOString() }}{% endif %}<span class="tag"></<span class="name">updated</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">id</span>></span>{{ url | uriencode }}<span class="tag"></<span class="name">id</span>></span></span><br><span class="line"> {% if config.author %}</span><br><span class="line"> <span class="tag"><<span class="name">author</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">name</span>></span>{{ config.author }}<span class="tag"></<span class="name">name</span>></span></span><br><span class="line"> {% if config.email %}<span class="tag"><<span class="name">email</span>></span>{{ config.email }}<span class="tag"></<span class="name">email</span>></span>{% endif %}</span><br><span class="line"> <span class="tag"></<span class="name">author</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> <span class="tag"><<span class="name">generator</span> <span class="attr">uri</span>=<span class="string">"https://hexo.io/"</span>></span>Hexo<span class="tag"></<span class="name">generator</span>></span></span><br><span class="line"> {% for post in posts.toArray() %}</span><br><span class="line"> <span class="tag"><<span class="name">entry</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>{{ post.title }}<span class="tag"></<span class="name">title</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">link</span> <span class="attr">href</span>=<span class="string">"{{ post.permalink | uriencode }}"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">id</span>></span>{{ post.permalink | uriencode }}<span class="tag"></<span class="name">id</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">published</span>></span>{{ post.date.toISOString() }}<span class="tag"></<span class="name">published</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">updated</span>></span>{% if post.updated %}{{ post.updated.toISOString() }}{% else %}{{ post.date.toISOString() }}{% endif %}<span class="tag"></<span class="name">updated</span>></span></span><br><span class="line"> {% if config.feed.content and post.content %}</span><br><span class="line"> <span class="tag"><<span class="name">content</span> <span class="attr">type</span>=<span class="string">"html"</span>></span><![CDATA[{{ post.content | noControlChars | safe }}]]><span class="tag"></<span class="name">content</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> {% if post.description %}</span><br><span class="line"> <span class="tag"><<span class="name">summary</span> <span class="attr">type</span>=<span class="string">"html"</span>></span>{{ post.description }}<span class="tag"></<span class="name">summary</span>></span></span><br><span class="line"> {% elif post.intro %}</span><br><span class="line"> <span class="tag"><<span class="name">summary</span> <span class="attr">type</span>=<span class="string">"html"</span>></span>{{ post.intro }}<span class="tag"></<span class="name">summary</span>></span></span><br><span class="line"> {% elif post.excerpt %}</span><br><span class="line"> <span class="tag"><<span class="name">summary</span> <span class="attr">type</span>=<span class="string">"html"</span>></span>{{ post.excerpt }}<span class="tag"></<span class="name">summary</span>></span></span><br><span class="line"> {% elif post.content %}</span><br><span class="line"> {% set short_content = post.content.substring(0, config.feed.content_limit) %}</span><br><span class="line"> {% if config.feed.content_limit_delim %}</span><br><span class="line"> {% set delim_pos = short_content.lastIndexOf(config.feed.content_limit_delim) %}</span><br><span class="line"> {% if delim_pos > -1 %}</span><br><span class="line"> <span class="tag"><<span class="name">summary</span> <span class="attr">type</span>=<span class="string">"html"</span>></span>{{ short_content.substring(0, delim_pos) }}<span class="tag"></<span class="name">summary</span>></span></span><br><span class="line"> {% else %}</span><br><span class="line"> <span class="tag"><<span class="name">summary</span> <span class="attr">type</span>=<span class="string">"html"</span>></span>{{ short_content }}<span class="tag"></<span class="name">summary</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> {% else %}</span><br><span class="line"> <span class="tag"><<span class="name">summary</span> <span class="attr">type</span>=<span class="string">"html"</span>></span>{{ short_content }}<span class="tag"></<span class="name">summary</span>></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> {% endif %}</span><br><span class="line"> {% if post.image %}</span><br><span class="line"> <span class="tag"><<span class="name">content</span> <span class="attr">src</span>=<span class="string">"{{ post.image | formatUrl }}"</span> <span class="attr">type</span>=<span class="string">"image"</span>/></span></span><br><span class="line"> {% endif %}</span><br><span class="line"> {% for category in post.categories.toArray() %}</span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">term</span>=<span class="string">"{{ category.name }}"</span> <span class="attr">scheme</span>=<span class="string">"{{ category.permalink }}"</span>/></span></span><br><span class="line"> {% endfor %}</span><br><span class="line"> {% for tag in post.tags.toArray() %}</span><br><span class="line"> <span class="tag"><<span class="name">category</span> <span class="attr">term</span>=<span class="string">"{{ tag.name }}"</span> <span class="attr">scheme</span>=<span class="string">"{{ tag.permalink }}"</span>/></span></span><br><span class="line"> {% endfor %}</span><br><span class="line"> <span class="tag"></<span class="name">entry</span>></span></span><br><span class="line"> {% endfor %}</span><br><span class="line"><span class="tag"></<span class="name">feed</span>></span></span><br></pre></td></tr></table></figure><p>完成后,本地运行看看效果。</p><p><img src="https://s2.loli.net/2024/10/16/cOm5lZyUB3iXxd4.png"></p><p>确认添加上Follow的认证代码后,<code>hexo d</code>部署完成后回到Follow点击<code>认证</code>即可。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://sekibetu.com/followis.html">三分钟,让Follow认证我的Hexo博客订阅源</a></p>]]></content>
<summary type="html"><p>首先在Follow添加订阅源 :<a href="https://xmaihh.github.io/blog/atom.xml">https://xmaihh.github.io/blog/atom.xml</a></p>
<p><img src="https://s2.l</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Hexo" scheme="https://xmaihh.github.io/blog/tags/Hexo/"/>
</entry>
<entry>
<title>Portainer重置admin登录密码</title>
<link href="https://xmaihh.github.io/blog/2024/09/10/portainer-chong-zhi-admin-deng-lu-mi-ma/"/>
<id>https://xmaihh.github.io/blog/2024/09/10/portainer-chong-zhi-admin-deng-lu-mi-ma/</id>
<published>2024-09-09T17:20:00.000Z</published>
<updated>2024-09-09T17:20:00.000Z</updated>
<content type="html"><![CDATA[<p> 参考Portainer官网解决方法:<a href="https://docs.portainer.io/advanced/reset-admin">https://docs.portainer.io/advanced/reset-admin</a></p><h1 id="停止Portainer容器"><a href="#停止Portainer容器" class="headerlink" title="停止Portainer容器"></a>停止Portainer容器</h1><p>先用<code>docker ps -a</code>查看所有容器,找到Portainer对应信息</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker stop <span class="string">"id-portainer-container"</span></span><br></pre></td></tr></table></figure><h1 id="找到Portainer容器的data目录挂载位置"><a href="#找到Portainer容器的data目录挂载位置" class="headerlink" title="找到Portainer容器的data目录挂载位置"></a>找到Portainer容器的data目录挂载位置</h1><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker inspect <span class="string">"id-portainer-container"</span></span><br></pre></td></tr></table></figure><p>挂载位置在这一行:<code>"Source": "/var/lib/docker/volumes/portainer_data/_data"</code></p><h1 id="执行重置密码"><a href="#执行重置密码" class="headerlink" title="执行重置密码"></a>执行重置密码</h1><p>这里用到了一个镜像 <a href="https://github.com/portainer/helper-reset-password">portainer/helper-reset-password</a></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">docker pull portainer/helper-reset-password</span><br><span class="line">docker run --<span class="built_in">rm</span> -v /var/lib/docker/volumes/portainer_data/_data:/data portainer/helper-reset-password</span><br></pre></td></tr></table></figure><blockquote><p><code>/var/lib/docker/volumes/portainer_data/_data</code>这里要替换成你在上一个步骤找到的挂载位置。</p></blockquote><p>命令执行成功输出如下:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">2024/09/10 00:13:58 Password successfully updated for user: admin</span><br><span class="line">2024/09/10 00:13:58 Use the following password to login: &_4#\3^5V8vLTd)E"NWiJBs26G*9HPl1</span><br></pre></td></tr></table></figure><p>现在admin登录的密码就为:<code>&_4#\3^5V8vLTd)E"NWiJBs26G*9HPl1</code></p><h1 id="启动Portainer容器"><a href="#启动Portainer容器" class="headerlink" title="启动Portainer容器"></a>启动Portainer容器</h1><p>尝试使用新密码登录</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker start <span class="string">"id-portainer-container"</span></span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><p> 参考Portainer官网解决方法:<a href="https://docs.portainer.io/advanced/reset-admin">https://docs.portainer.io/advanced/reset-admin</a></p>
<h1 id</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Linux" scheme="https://xmaihh.github.io/blog/tags/Linux/"/>
<category term="Docker" scheme="https://xmaihh.github.io/blog/tags/Docker/"/>
</entry>
<entry>
<title>华硕路由器修改Hosts</title>
<link href="https://xmaihh.github.io/blog/2024/08/02/hua-shuo-lu-you-qi-xiu-gai-hosts/"/>
<id>https://xmaihh.github.io/blog/2024/08/02/hua-shuo-lu-you-qi-xiu-gai-hosts/</id>
<published>2024-08-02T01:23:24.000Z</published>
<updated>2024-08-02T01:23:24.000Z</updated>
<content type="html"><![CDATA[<p><img src="https://s2.loli.net/2024/08/02/k8pg76arGJNqnXP.png" alt="ASUS"></p><p>前提条件:</p><ul><li>启用 SSH</li><li>SSH 用户名密码和登陆路由器后台的帐号密码一致</li></ul><p>打开华硕的 <code>SSH</code> 功能,具体路径是路由管理页面的 <code>高级设置 -> 系统管理 -> 系统设置 -> 服务 -> 启用SSH</code>。</p><p>通过 <code>SSH</code> 连接路由器后,修改 <code>/etc/hosts</code> 文件即可实现修改 <code>hosts</code> 功能。</p><p>但是会发现修改后的 <code>hosts</code> 仅对路由器生效,但是对下端设备均不生效,此时键入以下命令即可</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">killall -SIGHUP dnsmasq</span><br></pre></td></tr></table></figure><p><strong>路由器重启后以上对 <code>hosts</code> 文件的修改都会丢失,文件会恢复成固件默认的值。</strong></p><p>要达到重启不失效的目地,在 <code>/jffs/</code> 目录下创建一个名为 <code>dnsmasq.conf.add</code> 的文件,内容为 <code>addn-hosts=/jffs/configs/hosts</code> 并且保存。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">vi /jffs/dnsmasq.conf.add</span><br></pre></td></tr></table></figure><p>进入该目录下的 <code>configs</code> 文件夹(<code>/jffs/configs</code>),创建一个名为 <code>hosts</code> 的文件,并且在该文件中设置自定义域名解析,并且保存。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">192.168.125.1 router.asus.com</span><br></pre></td></tr></table></figure><p>重启 DNS 服务</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">service restart_dnsmasq</span><br></pre></td></tr></table></figure><p>自定义解析生效<br>至此,在此路由器下的全部设备在访问相关域名时会首先使用路由器中 Hosts 文件中的自定义设置。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://blog.irain.in/archives/ASUS_router_edit_Hosts.html">华硕路由器修改Hosts以达到局域网内自定义解析</a><br><a href="https://www.cnblogs.com/xiangliudev/p/16885325.html">华硕路由器修改hosts</a></p>]]></content>
<summary type="html"><p><img src="https://s2.loli.net/2024/08/02/k8pg76arGJNqnXP.png" alt="ASUS"></p>
<p>前提条件:</p>
<ul>
<li>启用 SSH</li>
<li>SSH 用户名密码和登陆路由器后台的帐号密</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Openwrt" scheme="https://xmaihh.github.io/blog/tags/Openwrt/"/>
</entry>
<entry>
<title>Flutter使用intl_generator在Windows下报错Cannot open file, path = 'l10n-arb/intl_*.arb' (OS Error: 文件名、目录名或卷标语法不正确</title>
<link href="https://xmaihh.github.io/blog/2024/07/30/flutter-shi-yong-intl-generator-zai-windows-xia-bao-cuo-cannot-open-file-path-l10n-arb-intl-arb-os-error-wen-jian-ming-mu-lu-ming-huo-juan-biao-yu-fa-bu-zheng-que/"/>
<id>https://xmaihh.github.io/blog/2024/07/30/flutter-shi-yong-intl-generator-zai-windows-xia-bao-cuo-cannot-open-file-path-l10n-arb-intl-arb-os-error-wen-jian-ming-mu-lu-ming-huo-juan-biao-yu-fa-bu-zheng-que/</id>
<published>2024-07-30T06:17:14.000Z</published>
<updated>2024-07-30T06:17:14.000Z</updated>
<content type="html"><![CDATA[<p>使用intl_generator 包从代码中提取要国际化的字符串到单独的arb文件和根据arb文件生成对应语言的dart代码</p><p>详细教程:<a href="https://book.flutterchina.club/chapter13/intl.html">https://book.flutterchina.club/chapter13/intl.html</a></p><p>在Windows的PowerShell环境下执行:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">dart run intl_generator:generate_from_arb --output-dir=lib/l10n --no-use-deferred-loading lib/l10n/localization_intl.dart l10n-arb/intl_*.arb</span><br></pre></td></tr></table></figure><p>报错信息:</p><figure class="highlight nsis"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">File</span><span class="params">System</span>Exception: Cannot <span class="literal">open</span> <span class="keyword">file</span>, path = <span class="string">'l10n-arb/intl_*.arb'</span> (OS Error: 文件名、目录名或卷标语法不正确。</span><br><span class="line">, errno = <span class="number">123</span>)</span><br></pre></td></tr></table></figure><p>原因:</p><p>在 PowerShell 中,通配符的处理方式与在其他命令行环境中有所不同。Windows下不会识别*通配符,上面写的<code>intl_*.arb</code>,导致错误。</p><p>解决办法:</p><p>请用<code>git bash</code>执行以上命令。</p><p>或者使用 Get-ChildItem 或 Get-Item 来处理Windows下的文件通配符。</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Get-ChildItem</span> l10n<span class="literal">-arb</span> <span class="literal">-Filter</span> intl_*.arb | <span class="built_in">ForEach-Object</span> {</span><br><span class="line"> dart run intl_generator:generate_from_arb <span class="literal">--output-dir</span>=lib/l10n <span class="literal">--no-use-deferred-loading</span> lib/l10n/localization_intl.dart <span class="variable">$_</span>.FullName</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://github.com/dart-lang/i18n/issues/496">intl_translation:generate_from_arb with wildcard argument doesn’t work on Windows</a></p>]]></content>
<summary type="html"><p>使用intl_generator 包从代码中提取要国际化的字符串到单独的arb文件和根据arb文件生成对应语言的dart代码</p>
<p>详细教程:<a href="https://book.flutterchina.club/chapter13/intl.html">h</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Flutter" scheme="https://xmaihh.github.io/blog/tags/Flutter/"/>
</entry>
<entry>
<title>在Hexo中使用PlantUML</title>
<link href="https://xmaihh.github.io/blog/2024/07/26/zai-hexo-zhong-shi-yong-plantuml/"/>
<id>https://xmaihh.github.io/blog/2024/07/26/zai-hexo-zhong-shi-yong-plantuml/</id>
<published>2024-07-26T07:13:39.000Z</published>
<updated>2024-07-26T07:13:39.000Z</updated>
<content type="html"><![CDATA[<p><a href="https://plantuml.com/zh/">PlantUML</a>是一个通用性很强的工具,可以快速、直接地创建各种图表。用来画组件图、部署图、状态图、时序图、甘特图等UML以及非UML图。</p><p>线上版 <a href="https://www.planttext.com/">https://www.planttext.com/</a><br><img src="https://s2.loli.net/2024/07/26/9VevsbduHw7R8lE.png" alt="示例"></p><h1 id="Hexo-PlantUML插件"><a href="#Hexo-PlantUML插件" class="headerlink" title="Hexo PlantUML插件"></a>Hexo PlantUML插件</h1><h2 id="hexo-tag-plantuml"><a href="#hexo-tag-plantuml" class="headerlink" title="hexo-tag-plantuml"></a><a href="https://github.com/two/hexo-tag-plantuml">hexo-tag-plantuml</a></h2><p>安装:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-tag-plantuml --save</span><br></pre></td></tr></table></figure><p>编辑Hexo目录下的<code>_config.yml</code>:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">tag_plantuml:</span></span><br><span class="line"><span class="attr">type:</span> <span class="string">static</span></span><br></pre></td></tr></table></figure><p>用法:</p><figure class="highlight django"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="template-tag">{% <span class="name">plantuml</span> %}</span><span class="language-xml"></span></span><br><span class="line"><span class="language-xml"> Bob->Alice : hello</span></span><br><span class="line"><span class="language-xml"></span><span class="template-tag">{% <span class="name">endplantuml</span> %}</span></span><br></pre></td></tr></table></figure><p>显示:</p><img class="kroki" src='data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXMtYXNjaWkiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgaGVpZ2h0PSIxMjBweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjEwOXB4O2hlaWdodDoxMjBweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxMDkgMTIwIiB3aWR0aD0iMTA5cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTtzdHJva2UtZGFzaGFycmF5OjUuMCw1LjA7IiB4MT0iMjYiIHgyPSIyNiIgeTE9IjM2LjI5NjkiIHkyPSI4NS40Mjk3Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7c3Ryb2tlLWRhc2hhcnJheTo1LjAsNS4wOyIgeDE9IjgwIiB4Mj0iODAiIHkxPSIzNi4yOTY5IiB5Mj0iODUuNDI5NyIvPjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDIiIHg9IjUiIHk9IjUiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSIyOCIgeD0iMTIiIHk9IjI0Ljk5NTEiPkJvYjwvdGV4dD48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQyIiB4PSI1IiB5PSI4NC40Mjk3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMjgiIHg9IjEyIiB5PSIxMDQuNDI0OCI+Qm9iPC90ZXh0PjxyZWN0IGZpbGw9IiNFMkUyRjAiIGhlaWdodD0iMzAuMjk2OSIgcng9IjIuNSIgcnk9IjIuNSIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDowLjU7IiB3aWR0aD0iNDYiIHg9IjU3IiB5PSI1Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzIiIHg9IjY0IiB5PSIyNC45OTUxIj5BbGljZTwvdGV4dD48cmVjdCBmaWxsPSIjRTJFMkYwIiBoZWlnaHQ9IjMwLjI5NjkiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjQ2IiB4PSI1NyIgeT0iODQuNDI5NyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjMyIiB4PSI2NCIgeT0iMTA0LjQyNDgiPkFsaWNlPC90ZXh0Pjxwb2x5Z29uIGZpbGw9IiMxODE4MTgiIHBvaW50cz0iNjgsNjMuNDI5Nyw3OCw2Ny40Mjk3LDY4LDcxLjQyOTcsNzIsNjcuNDI5NyIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PGxpbmUgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7IiB4MT0iMjYiIHgyPSI3NCIgeTE9IjY3LjQyOTciIHkyPSI2Ny40Mjk3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTMiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iMzAiIHg9IjMzIiB5PSI2Mi4zNjM4Ij5oZWxsbzwvdGV4dD48IS0tU1JDPVtTeWZGcWhMcHBDYkNKYk1tS2lYOHBTZDkxbTAwXS0tPjwvZz48L3N2Zz4='><h2 id="hexo-filter-plantuml"><a href="#hexo-filter-plantuml" class="headerlink" title="hexo-filter-plantuml"></a><a href="https://github.com/miao1007/hexo-filter-plantuml">hexo-filter-plantuml</a></h2><p>安装:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-filter-plantuml --save </span><br></pre></td></tr></table></figure><p>无需配置即可使用,或者你可以参考<a href="https://github.com/miao1007/hexo-filter-plantuml?tab=readme-ov-file#advanced-configuration">Advanced configuration</a></p><p>用法:</p><p><code>puml</code> 和 <code>plantuml</code> 指令都有效。</p><p><img src="https://s2.loli.net/2024/07/26/kD87JBaFmSUWgrX.png" alt="示例1"></p><p>或者</p><p><img src="https://s2.loli.net/2024/07/26/V5anfld1jQeAJyP.png" alt="示例2"></p><img class="kroki" src='data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXMtYXNjaWkiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBjb250ZW50U3R5bGVUeXBlPSJ0ZXh0L2NzcyIgaGVpZ2h0PSIyMThweCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSIgc3R5bGU9IndpZHRoOjE3NHB4O2hlaWdodDoyMThweDtiYWNrZ3JvdW5kOiNGRkZGRkY7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAxNzQgMjE4IiB3aWR0aD0iMTc0cHgiIHpvb21BbmRQYW49Im1hZ25pZnkiPjxkZWZzLz48Zz48IS0tY2xhc3MgT2JqZWN0LS0+PGcgaWQ9ImVsZW1fT2JqZWN0Ij48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjY0LjI5NjkiIGlkPSJPYmplY3QiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9Ijc3IiB4PSI0OC41IiB5PSI3Ii8+PGVsbGlwc2UgY3g9IjYzLjUiIGN5PSIyMyIgZmlsbD0iI0FERDFCMiIgcng9IjExIiByeT0iMTEiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjxwYXRoIGQ9Ik02Ni40Njg4LDI4LjY0MDYgUTY1Ljg5MDYsMjguOTM3NSA2NS4yNSwyOS4wNzgxIFE2NC42MDk0LDI5LjIzNDQgNjMuOTA2MywyOS4yMzQ0IFE2MS40MDYzLDI5LjIzNDQgNjAuMDc4MSwyNy41OTM4IFE1OC43NjU2LDI1LjkzNzUgNTguNzY1NiwyMi44MTI1IFE1OC43NjU2LDE5LjY4NzUgNjAuMDc4MSwxOC4wMzEzIFE2MS40MDYzLDE2LjM3NSA2My45MDYzLDE2LjM3NSBRNjQuNjA5NCwxNi4zNzUgNjUuMjUsMTYuNTMxMyBRNjUuOTA2MywxNi42ODc1IDY2LjQ2ODgsMTYuOTg0NCBMNjYuNDY4OCwxOS43MDMxIFE2NS44NDM4LDE5LjEyNSA2NS4yNSwxOC44NTk0IFE2NC42NTYzLDE4LjU3ODEgNjQuMDMxMywxOC41NzgxIFE2Mi42ODc1LDE4LjU3ODEgNjIsMTkuNjU2MyBRNjEuMzEyNSwyMC43MTg4IDYxLjMxMjUsMjIuODEyNSBRNjEuMzEyNSwyNC45MDYzIDYyLDI1Ljk4NDQgUTYyLjY4NzUsMjcuMDQ2OSA2NC4wMzEzLDI3LjA0NjkgUTY0LjY1NjMsMjcuMDQ2OSA2NS4yNSwyNi43ODEzIFE2NS44NDM4LDI2LjUgNjYuNDY4OCwyNS45MjE5IEw2Ni40Njg4LDI4LjY0MDYgWiAiIGZpbGw9IiMwMDAwMDAiLz48dGV4dCBmaWxsPSIjMDAwMDAwIiBmb250LWZhbWlseT0ic2Fucy1zZXJpZiIgZm9udC1zaXplPSIxNCIgbGVuZ3RoQWRqdXN0PSJzcGFjaW5nIiB0ZXh0TGVuZ3RoPSI0NSIgeD0iNzcuNSIgeT0iMjcuODQ2NyI+T2JqZWN0PC90ZXh0PjxsaW5lIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgeDE9IjQ5LjUiIHgyPSIxMjQuNSIgeTE9IjM5IiB5Mj0iMzkiLz48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTsiIHgxPSI0OS41IiB4Mj0iMTI0LjUiIHkxPSI0NyIgeTI9IjQ3Ii8+PHRleHQgZmlsbD0iIzAwMDAwMCIgZm9udC1mYW1pbHk9InNhbnMtc2VyaWYiIGZvbnQtc2l6ZT0iMTQiIGxlbmd0aEFkanVzdD0ic3BhY2luZyIgdGV4dExlbmd0aD0iNTYiIHg9IjU0LjUiIHk9IjYzLjk5NTEiPmVxdWFscygpPC90ZXh0PjwvZz48IS0tY2xhc3MgQXJyYXlMaXN0LS0+PGcgaWQ9ImVsZW1fQXJyYXlMaXN0Ij48cmVjdCBmaWxsPSIjRjFGMUYxIiBoZWlnaHQ9IjgwLjU5MzgiIGlkPSJBcnJheUxpc3QiIHJ4PSIyLjUiIHJ5PSIyLjUiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MC41OyIgd2lkdGg9IjE2MCIgeD0iNyIgeT0iMTMxLjMiLz48ZWxsaXBzZSBjeD0iNTQuMjUiIGN5PSIxNDcuMyIgZmlsbD0iI0FERDFCMiIgcng9IjExIiByeT0iMTEiIHN0eWxlPSJzdHJva2U6IzE4MTgxODtzdHJva2Utd2lkdGg6MS4wOyIvPjxwYXRoIGQ9Ik01Ny4yMTg4LDE1Mi45NDA2IFE1Ni42NDA2LDE1My4yMzc1IDU2LDE1My4zNzgxIFE1NS4zNTk0LDE1My41MzQ0IDU0LjY1NjMsMTUzLjUzNDQgUTUyLjE1NjMsMTUzLjUzNDQgNTAuODI4MSwxNTEuODkzNyBRNDkuNTE1NiwxNTAuMjM3NSA0OS41MTU2LDE0Ny4xMTI1IFE0OS41MTU2LDE0My45ODc1IDUwLjgyODEsMTQyLjMzMTIgUTUyLjE1NjMsMTQwLjY3NSA1NC42NTYzLDE0MC42NzUgUTU1LjM1OTQsMTQwLjY3NSA1NiwxNDAuODMxMiBRNTYuNjU2MywxNDAuOTg3NSA1Ny4yMTg4LDE0MS4yODQ0IEw1Ny4yMTg4LDE0NC4wMDMxIFE1Ni41OTM4LDE0My40MjUgNTYsMTQzLjE1OTQgUTU1LjQwNjMsMTQyLjg3ODEgNTQuNzgxMywxNDIuODc4MSBRNTMuNDM3NSwxNDIuODc4MSA1Mi43NSwxNDMuOTU2MiBRNTIuMDYyNSwxNDUuMDE4NyA1Mi4wNjI1LDE0Ny4xMTI1IFE1Mi4wNjI1LDE0OS4yMDYyIDUyLjc1LDE1MC4yODQ0IFE1My40Mzc1LDE1MS4zNDY5IDU0Ljc4MTMsMTUxLjM0NjkgUTU1LjQwNjMsMTUxLjM0NjkgNTYsMTUxLjA4MTIgUTU2LjU5MzgsMTUwLjggNTcuMjE4OCwxNTAuMjIxOSBMNTcuMjE4OCwxNTIuOTQwNiBaICIgZmlsbD0iIzAwMDAwMCIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjU3IiB4PSI3NC43NSIgeT0iMTUyLjE0NjciPkFycmF5TGlzdDwvdGV4dD48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTsiIHgxPSI4IiB4Mj0iMTY2IiB5MT0iMTYzLjMiIHkyPSIxNjMuMyIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjE0OCIgeD0iMTMiIHk9IjE4MC4yOTUxIj5PYmplY3RbXSBlbGVtZW50RGF0YTwvdGV4dD48bGluZSBzdHlsZT0ic3Ryb2tlOiMxODE4MTg7c3Ryb2tlLXdpZHRoOjAuNTsiIHgxPSI4IiB4Mj0iMTY2IiB5MT0iMTg3LjU5NjkiIHkyPSIxODcuNTk2OSIvPjx0ZXh0IGZpbGw9IiMwMDAwMDAiIGZvbnQtZmFtaWx5PSJzYW5zLXNlcmlmIiBmb250LXNpemU9IjE0IiBsZW5ndGhBZGp1c3Q9InNwYWNpbmciIHRleHRMZW5ndGg9IjM4IiB4PSIxMyIgeT0iMjA0LjU5MiI+c2l6ZSgpPC90ZXh0PjwvZz48IS0tcmV2ZXJzZSBsaW5rIE9iamVjdCB0byBBcnJheUxpc3QtLT48ZyBpZD0ibGlua19PYmplY3RfQXJyYXlMaXN0Ij48cGF0aCBjb2RlTGluZT0iMSIgZD0iTTg3LDg5LjcyIEM4NywxMDcuNTIgODcsMTExLjkyIDg3LDEzMC44OSAiIGZpbGw9Im5vbmUiIGlkPSJPYmplY3QtYmFja3RvLUFycmF5TGlzdCIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PHBvbHlnb24gZmlsbD0ibm9uZSIgcG9pbnRzPSI4Nyw3MS43Miw4MSw4OS43Miw5Myw4OS43Miw4Nyw3MS43MiIgc3R5bGU9InN0cm9rZTojMTgxODE4O3N0cm9rZS13aWR0aDoxLjA7Ii8+PC9nPjwhLS1TUkM9W3lxX0FJYXFrS1IyZnFUTExTMm1nSWdwcW9JbWt1VkE3WTVlZmYxUU05a09LUXNYb21VTTBXWDNQdzVZNXI5cEt0REl5NGZWNGFhR0sxU01QTFFhUWNXMDBdLS0+PC9nPjwvc3ZnPg=='><h2 id="hexo-filter-kroki-推荐"><a href="#hexo-filter-kroki-推荐" class="headerlink" title="hexo-filter-kroki[推荐]"></a><a href="https://github.com/miao1007/hexo-filter-kroki"><strong>hexo-filter-kroki[<strong>推荐</strong>]</strong></a></h2><p>这个插件是上一个插件<code>hexo-filter-plantuml</code>的升级版本,不仅支持plantuml图,还支持其他的绘图方式。</p><p>安装:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install hexo-filter-kroki --save</span><br></pre></td></tr></table></figure><p>用法和<code>hexo-filter-plantuml</code>一样,无需配置即可使用,或者你可以参考<a href="https://github.com/miao1007/hexo-filter-kroki?tab=readme-ov-file#advanced-configuration">Advanced configuration</a></p><p>查看支持的图表类型:<a href="https://kroki.io/health">https://kroki.io/health</a></p><p>查看图表示例:<a href="https://kroki.io/examples.html">https://kroki.io/examples.html</a></p><p><img src="https://s2.loli.net/2024/07/26/86CdE4XPKtpmcQ7.png" alt="wavedrom示例"></p><img class="kroki" src='data:image/svg+xml;base64,PHN2ZyBpZD0ic3ZnY29udGVudF8wIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBoZWlnaHQ9IjE1MCIgd2lkdGg9IjU0MCIgdmlld0JveD0iMCAwIDU0MCAxNTAiIG92ZXJmbG93PSJoaWRkZW4iIGNsYXNzPSJXYXZlRHJvbSI+PHN0eWxlIHR5cGU9InRleHQvY3NzIj50ZXh0e2ZvbnQtc2l6ZToxMXB0O2ZvbnQtc3R5bGU6bm9ybWFsO2ZvbnQtdmFyaWFudDpub3JtYWw7Zm9udC13ZWlnaHQ6bm9ybWFsO2ZvbnQtc3RyZXRjaDpub3JtYWw7dGV4dC1hbGlnbjpjZW50ZXI7ZmlsbC1vcGFjaXR5OjE7Zm9udC1mYW1pbHk6SGVsdmV0aWNhfS5oMXtmb250LXNpemU6MzNwdDtmb250LXdlaWdodDpib2xkfS5oMntmb250LXNpemU6MjdwdDtmb250LXdlaWdodDpib2xkfS5oM3tmb250LXNpemU6MjBwdDtmb250LXdlaWdodDpib2xkfS5oNHtmb250LXNpemU6MTRwdDtmb250LXdlaWdodDpib2xkfS5oNXtmb250LXNpemU6MTFwdDtmb250LXdlaWdodDpib2xkfS5oNntmb250LXNpemU6OHB0O2ZvbnQtd2VpZ2h0OmJvbGR9Lm11dGVke2ZpbGw6I2FhYX0ud2FybmluZ3tmaWxsOiNmNmI5MDB9LmVycm9ye2ZpbGw6I2Y2MDAwMH0uaW5mb3tmaWxsOiMwMDQxYzR9LnN1Y2Nlc3N7ZmlsbDojMDBhYjAwfS5zMXtmaWxsOm5vbmU7c3Ryb2tlOiMwMDA7c3Ryb2tlLXdpZHRoOjE7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOm1pdGVyO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLW9wYWNpdHk6MTtzdHJva2UtZGFzaGFycmF5Om5vbmV9LnMye2ZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MC41O3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lfS5zM3tjb2xvcjojMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6MSwgMztzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlfS5zNHtjb2xvcjojMDAwO2ZpbGw6bm9uZTtzdHJva2U6IzAwMDtzdHJva2Utd2lkdGg6MTtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46bWl0ZXI7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2Utb3BhY2l0eToxO3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlfS5zNXtmaWxsOiNmZmY7c3Ryb2tlOm5vbmV9LnM2e2ZpbGw6IzAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZX0uczd7Y29sb3I6IzAwMDtmaWxsOiNmZmY7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjFweDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZX0uczh7Y29sb3I6IzAwMDtmaWxsOiNmZmZmYjQ7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjFweDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZX0uczl7Y29sb3I6IzAwMDtmaWxsOiNmZmUwYjk7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjFweDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZX0uczEwe2NvbG9yOiMwMDA7ZmlsbDojYjllMGZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxcHg7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGV9LnMxMXtjb2xvcjojMDAwO2ZpbGw6I2NjZmRmZTtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MXB4O21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlfS5zMTJ7Y29sb3I6IzAwMDtmaWxsOiNjZGZkYzU7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOm5vbnplcm87c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjFweDttYXJrZXI6bm9uZTt2aXNpYmlsaXR5OnZpc2libGU7ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZX0uczEze2NvbG9yOiMwMDA7ZmlsbDojZjBjMWZiO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpub256ZXJvO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoxcHg7bWFya2VyOm5vbmU7dmlzaWJpbGl0eTp2aXNpYmxlO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGV9LnMxNHtjb2xvcjojMDAwO2ZpbGw6I2Y1YzJjMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6bm9uemVybztzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MXB4O21hcmtlcjpub25lO3Zpc2liaWxpdHk6dmlzaWJsZTtkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlfS5zMTV7ZmlsbDojMDA0MWM0O2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lfS5zMTZ7ZmlsbDpub25lO3N0cm9rZTojMDA0MWM0O3N0cm9rZS13aWR0aDoxO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjptaXRlcjtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1vcGFjaXR5OjE7c3Ryb2tlLWRhc2hhcnJheTpub25lfTwvc3R5bGU+PGRlZnM+PGcgaWQ9InNvY2tldCI+PHJlY3QgeT0iMTUiIHg9IjYiIGhlaWdodD0iMjAiIHdpZHRoPSIyMCIvPjwvZz48ZyBpZD0icGNsayI+PHBhdGggZD0iTTAsMjAgMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0ibmNsayI+PHBhdGggZD0ibTAsMCAwLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMDAwIj48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IjBtMCI+PHBhdGggZD0ibTAsMjAgMywwIDMsLTEwIDMsMTAgMTEsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSIwbTEiPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMG14Ij48cGF0aCBkPSJNMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTIwLDE1IC01LDUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwxMCAxMCwyMCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDUgNSwyMCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDAgNCwxNiIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTE1LDAgNiw5IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMTAsMCA5LDEiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMG1kIj48cGF0aCBkPSJtOCwyMCAxMCwwIiBjbGFzcz0iczMiLz48cGF0aCBkPSJtMCwyMCA1LDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMG11Ij48cGF0aCBkPSJtMCwyMCAzLDAgQyA3LDEwIDEwLjEwNzYwMywwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMG16Ij48cGF0aCBkPSJtMCwyMCAzLDAgQyAxMCwxMCAxNSwxMCAyMCwxMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSIxMTEiPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSIxbTAiPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSIxbTEiPjxwYXRoIGQ9Ik0wLDAgMywwIDYsMTAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMW14Ij48cGF0aCBkPSJtMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMjAsMTUgLTUsNSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDEwIDEwLDIwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsNSA4LDE3IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMCA3LDEzIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMTUsMCA2LDkiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xMCwwIDUsNSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTMuNSwxLjUgNSwwIiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9IjFtZCI+PHBhdGggZD0ibTAsMCAzLDAgYyA0LDEwIDcsMjAgMTcsMjAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMW11Ij48cGF0aCBkPSJNMCwwIDUsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTgsMCAxOCwwIiBjbGFzcz0iczMiLz48L2c+PGcgaWQ9IjFteiI+PHBhdGggZD0ibTAsMCAzLDAgYyA3LDEwIDEyLDEwIDE3LDEwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9Inh4eCI+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCw1IDUsMCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMTAgMTAsMCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMTUgMTUsMCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgMjAsMCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTUsMjAgMjAsNSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTEwLDIwIDIwLDEwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJtMTUsMjAgNSwtNSIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJ4bTAiPjxwYXRoIGQ9Ik0wLDAgNCwwIDksMjAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDUgNCwxIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwxMCA1LDUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDYsOSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgNywxMyIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTUsMjAgOCwxNyIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJ4bTEiPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgNCwyMCA5LDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDUgNSwwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwxMCA5LDEiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDcsOCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgNSwxNSIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJ4bXgiPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsNSA1LDAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDEwIDEwLDAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDE1LDAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDIwIDIwLDAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik01LDIwIDIwLDUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xMCwyMCAyMCwxMCIgY2xhc3M9InMyIi8+PHBhdGggZD0ibTE1LDIwIDUsLTUiIGNsYXNzPSJzMiIvPjwvZz48ZyBpZD0ieG1kIj48cGF0aCBkPSJtMCwwIDQsMCBjIDMsMTAgNiwyMCAxNiwyMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsNSA0LDEiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDEwIDUuNSw0LjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDYuNSw4LjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDIwIDgsMTIiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Im01LDIwIDUsLTUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Im0xMCwyMCAyLjUsLTIuNSIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJ4bXUiPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgNCwwIEMgNywxMCAxMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDUgNSwwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwxMCAxMCwwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwxNSAxMCw1IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwyMCA2LDE0IiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9InhteiI+PHBhdGggZD0ibTAsMCA0LDAgYyA2LDEwIDExLDEwIDE2LDEwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCA0LDAgQyAxMCwxMCAxNSwxMCAyMCwxMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsNSA0LjUsMC41IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwxMCA2LjUsMy41IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwxNSA4LjUsNi41IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwyMCAxMS41LDguNSIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJkZGQiPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMyIvPjwvZz48ZyBpZD0iZG0wIj48cGF0aCBkPSJtMCwyMCAxMCwwIiBjbGFzcz0iczMiLz48cGF0aCBkPSJtMTIsMjAgOCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9ImRtMSI+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJkbXgiPjxwYXRoIGQ9Ik0zLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMjAsMTUgLTUsNSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDEwIDEwLDIwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsNSA1LDIwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMCA0LDE2IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMTUsMCA2LDkiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xMCwwIDksMSIgY2xhc3M9InMyIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJkbWQiPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMyIvPjwvZz48ZyBpZD0iZG11Ij48cGF0aCBkPSJtMCwyMCAzLDAgQyA3LDEwIDEwLjEwNzYwMywwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iZG16Ij48cGF0aCBkPSJtMCwyMCAzLDAgQyAxMCwxMCAxNSwxMCAyMCwxMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ1dXUiPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMzIi8+PC9nPjxnIGlkPSJ1bTAiPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ1bTEiPjxwYXRoIGQ9Ik0wLDAgMTAsMCIgY2xhc3M9InMzIi8+PHBhdGggZD0ibTEyLDAgOCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InVteCI+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTIwLDE1IC01LDUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwxMCAxMCwyMCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDUgOCwxNyIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDAgNywxMyIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTE1LDAgNiw5IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMTAsMCA1LDUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0zLjUsMS41IDUsMCIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJ1bWQiPjxwYXRoIGQ9Im0wLDAgMywwIGMgNCwxMCA3LDIwIDE3LDIwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InVtdSI+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczMiLz48L2c+PGcgaWQ9InVteiI+PHBhdGggZD0ibTAsMCAzLDAgYyA3LDEwIDEyLDEwIDE3LDEwIiBjbGFzcz0iczQiLz48L2c+PGcgaWQ9Inp6eiI+PHBhdGggZD0ibTAsMTAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ6bTAiPjxwYXRoIGQ9Im0wLDEwIDYsMCAzLDEwIDExLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iem0xIj48cGF0aCBkPSJNMCwxMCA2LDEwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InpteCI+PHBhdGggZD0ibTYsMTAgMywxMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwxMCA2LDEwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMjAsMTUgLTUsNSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDEwIDEwLDIwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsNSA4LDE3IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMCA3LDEzIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMTUsMCA2LjUsOC41IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMTAsMCA5LDEiIGNsYXNzPSJzMiIvPjwvZz48ZyBpZD0iem1kIj48cGF0aCBkPSJtMCwxMCA3LDAgYyAzLDUgOCwxMCAxMywxMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ6bXUiPjxwYXRoIGQ9Im0wLDEwIDcsMCBDIDEwLDUgMTUsMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InpteiI+PHBhdGggZD0ibTAsMTAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJnYXAiPjxwYXRoIGQ9Im03LC0yIC00LDAgYyAtNSwwIC01LDI0IC0xMCwyNCBsIDQsMCBDIDIsMjIgMiwtMiA3LC0yIHoiIGNsYXNzPSJzNSIvPjxwYXRoIGQ9Ik0tNywyMiBDIC0yLDIyIC0yLC0yIDMsLTIiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0tMywyMiBDIDIsMjIgMiwtMiA3LC0yIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IlBjbGsiPjxwYXRoIGQ9Ik0tMywxMiAwLDMgMywxMiBDIDEsMTEgLTEsMTEgLTMsMTIgeiIgY2xhc3M9InM2Ii8+PHBhdGggZD0iTTAsMjAgMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iTmNsayI+PHBhdGggZD0iTS0zLDggMCwxNyAzLDggQyAxLDkgLTEsOSAtMyw4IHoiIGNsYXNzPSJzNiIvPjxwYXRoIGQ9Im0wLDAgMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IjBtdi0yIj48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgMywyMCB6IiBjbGFzcz0iczciLz48cGF0aCBkPSJNMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSIxbXYtMiI+PHBhdGggZD0iTTIuODc1LDAgMjAsMCAyMCwyMCA5LDIwIHoiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0zLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0ieG12LTIiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCw1IDMuNSwxLjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDEwIDQuNSw1LjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDYsOSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgNCwxNiIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJkbXYtMiI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDMsMjAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0iTTMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idW12LTIiPjxwYXRoIGQ9Ik0zLDAgMjAsMCAyMCwyMCA5LDIwIHoiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0zLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iem12LTIiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0ibTYsMTAgMywxMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwxMCA2LDEwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZ2di0yIj48cGF0aCBkPSJNMjAsMjAgMCwyMCAwLDAgMjAsMCIgY2xhc3M9InM3Ii8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtMC0yIj48cGF0aCBkPSJNMCwyMCAwLDAgMywwIDksMjAiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Ik0wLDAgMywwIDksMjAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm0xLTIiPjxwYXRoIGQ9Ik0wLDAgMCwyMCAzLDIwIDksMCIgY2xhc3M9InM3Ii8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXgtMiI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgNiwxMCAzLDAiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTIwLDE1IC01LDUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwxMCAxMCwyMCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDUgOCwxNyIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDAgNywxMyIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTE1LDAgNyw4IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMTAsMCA5LDEiIGNsYXNzPSJzMiIvPjwvZz48ZyBpZD0idm1kLTIiPjxwYXRoIGQ9Im0wLDAgMCwyMCAyMCwwIEMgMTAsMjAgNywxMCAzLDAiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0wLDAgMywwIGMgNCwxMCA3LDIwIDE3LDIwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdS0yIj48cGF0aCBkPSJtMCwwIDAsMjAgMywwIEMgNywxMCAxMCwwIDIwLDAiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDcsMTAgMTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm16LTIiPjxwYXRoIGQ9Ik0wLDAgMywwIEMgMTAsMTAgMTUsMTAgMjAsMTAgMTUsMTAgMTAsMTAgMywyMCBMIDAsMjAiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0wLDAgMywwIGMgNywxMCAxMiwxMCAxNywxMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMywwIEMgMTAsMTAgMTUsMTAgMjAsMTAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMG12LTMiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCAzLDIwIHoiIGNsYXNzPSJzOCIvPjxwYXRoIGQ9Ik0zLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IjFtdi0zIj48cGF0aCBkPSJNMi44NzUsMCAyMCwwIDIwLDIwIDksMjAgeiIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ4bXYtMyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDUgMy41LDEuNSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMTAgNC41LDUuNSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMTUgNiw5IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwyMCA0LDE2IiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9ImRtdi0zIj48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgMywyMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJNMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ1bXYtMyI+PHBhdGggZD0iTTMsMCAyMCwwIDIwLDIwIDksMjAgeiIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ6bXYtMyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJtNiwxMCAzLDEwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDEwIDYsMTAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idnZ2LTMiPjxwYXRoIGQ9Ik0yMCwyMCAwLDIwIDAsMCAyMCwwIiBjbGFzcz0iczgiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm0wLTMiPjxwYXRoIGQ9Ik0wLDIwIDAsMCAzLDAgOSwyMCIgY2xhc3M9InM4Ii8+PHBhdGggZD0iTTAsMCAzLDAgOSwyMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTEtMyI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgOSwwIiBjbGFzcz0iczgiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZteC0zIj48cGF0aCBkPSJNMCwwIDAsMjAgMywyMCA2LDEwIDMsMCIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMjAsMTUgLTUsNSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTIwLDEwIDEwLDIwIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsNSA4LDE3IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMCA3LDEzIiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMTUsMCA3LDgiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xMCwwIDksMSIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJ2bWQtMyI+PHBhdGggZD0ibTAsMCAwLDIwIDIwLDAgQyAxMCwyMCA3LDEwIDMsMCIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTAsMCAzLDAgYyA0LDEwIDcsMjAgMTcsMjAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm11LTMiPjxwYXRoIGQ9Im0wLDAgMCwyMCAzLDAgQyA3LDEwIDEwLDAgMjAsMCIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTAsMjAgMywwIEMgNywxMCAxMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXotMyI+PHBhdGggZD0iTTAsMCAzLDAgQyAxMCwxMCAxNSwxMCAyMCwxMCAxNSwxMCAxMCwxMCAzLDIwIEwgMCwyMCIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTAsMCAzLDAgYyA3LDEwIDEyLDEwIDE3LDEwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAzLDAgQyAxMCwxMCAxNSwxMCAyMCwxMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSIwbXYtNCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDMsMjAgeiIgY2xhc3M9InM5Ii8+PHBhdGggZD0iTTMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMW12LTQiPjxwYXRoIGQ9Ik0yLjg3NSwwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczkiLz48cGF0aCBkPSJtMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9Inhtdi00Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsNSAzLjUsMS41IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwxMCA0LjUsNS41IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMCwxNSA2LDkiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDIwIDQsMTYiIGNsYXNzPSJzMiIvPjwvZz48ZyBpZD0iZG12LTQiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCAzLDIwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Ik0zLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InVtdi00Ij48cGF0aCBkPSJNMywwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczkiLz48cGF0aCBkPSJtMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9Inptdi00Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Im02LDEwIDMsMTAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMTAgNiwxMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2dnYtNCI+PHBhdGggZD0iTTIwLDIwIDAsMjAgMCwwIDIwLDAiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTAtNCI+PHBhdGggZD0iTTAsMjAgMCwwIDMsMCA5LDIwIiBjbGFzcz0iczkiLz48cGF0aCBkPSJNMCwwIDMsMCA5LDIwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtMS00Ij48cGF0aCBkPSJNMCwwIDAsMjAgMywyMCA5LDAiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm14LTQiPjxwYXRoIGQ9Ik0wLDAgMCwyMCAzLDIwIDYsMTAgMywwIiBjbGFzcz0iczkiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0yMCwxNSAtNSw1IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMTAgMTAsMjAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCw1IDgsMTciIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwwIDcsMTMiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xNSwwIDcsOCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTEwLDAgOSwxIiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9InZtZC00Ij48cGF0aCBkPSJtMCwwIDAsMjAgMjAsMCBDIDEwLDIwIDcsMTAgMywwIiBjbGFzcz0iczkiLz48cGF0aCBkPSJtMCwwIDMsMCBjIDQsMTAgNywyMCAxNywyMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXUtNCI+PHBhdGggZD0ibTAsMCAwLDIwIDMsMCBDIDcsMTAgMTAsMCAyMCwwIiBjbGFzcz0iczkiLz48cGF0aCBkPSJtMCwyMCAzLDAgQyA3LDEwIDEwLDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtei00Ij48cGF0aCBkPSJNMCwwIDMsMCBDIDEwLDEwIDE1LDEwIDIwLDEwIDE1LDEwIDEwLDEwIDMsMjAgTCAwLDIwIiBjbGFzcz0iczkiLz48cGF0aCBkPSJtMCwwIDMsMCBjIDcsMTAgMTIsMTAgMTcsMTAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDEwLDEwIDE1LDEwIDIwLDEwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IjBtdi01Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgMywyMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0iTTMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMW12LTUiPjxwYXRoIGQ9Ik0yLjg3NSwwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ4bXYtNSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCw1IDMuNSwxLjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDEwIDQuNSw1LjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDYsOSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgNCwxNiIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJkbXYtNSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDMsMjAgeiIgY2xhc3M9InMxMCIvPjxwYXRoIGQ9Ik0zLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InVtdi01Ij48cGF0aCBkPSJNMywwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ6bXYtNSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0ibTYsMTAgMywxMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwxMCA2LDEwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZ2di01Ij48cGF0aCBkPSJNMjAsMjAgMCwyMCAwLDAgMjAsMCIgY2xhc3M9InMxMCIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTAtNSI+PHBhdGggZD0iTTAsMjAgMCwwIDMsMCA5LDIwIiBjbGFzcz0iczEwIi8+PHBhdGggZD0iTTAsMCAzLDAgOSwyMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTEtNSI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgOSwwIiBjbGFzcz0iczEwIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXgtNSI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgNiwxMCAzLDAiIGNsYXNzPSJzMTAiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0yMCwxNSAtNSw1IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMTAgMTAsMjAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCw1IDgsMTciIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwwIDcsMTMiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xNSwwIDcsOCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTEwLDAgOSwxIiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9InZtZC01Ij48cGF0aCBkPSJtMCwwIDAsMjAgMjAsMCBDIDEwLDIwIDcsMTAgMywwIiBjbGFzcz0iczEwIi8+PHBhdGggZD0ibTAsMCAzLDAgYyA0LDEwIDcsMjAgMTcsMjAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm11LTUiPjxwYXRoIGQ9Im0wLDAgMCwyMCAzLDAgQyA3LDEwIDEwLDAgMjAsMCIgY2xhc3M9InMxMCIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDcsMTAgMTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm16LTUiPjxwYXRoIGQ9Ik0wLDAgMywwIEMgMTAsMTAgMTUsMTAgMjAsMTAgMTUsMTAgMTAsMTAgMywyMCBMIDAsMjAiIGNsYXNzPSJzMTAiLz48cGF0aCBkPSJtMCwwIDMsMCBjIDcsMTAgMTIsMTAgMTcsMTAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDEwLDEwIDE1LDEwIDIwLDEwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IjBtdi02Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgMywyMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0iTTMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMW12LTYiPjxwYXRoIGQ9Ik0yLjg3NSwwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ4bXYtNiI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCw1IDMuNSwxLjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDEwIDQuNSw1LjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDYsOSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgNCwxNiIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJkbXYtNiI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDMsMjAgeiIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Ik0zLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InVtdi02Ij48cGF0aCBkPSJNMywwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ6bXYtNiI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0ibTYsMTAgMywxMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwxMCA2LDEwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZ2di02Ij48cGF0aCBkPSJNMjAsMjAgMCwyMCAwLDAgMjAsMCIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTAtNiI+PHBhdGggZD0iTTAsMjAgMCwwIDMsMCA5LDIwIiBjbGFzcz0iczExIi8+PHBhdGggZD0iTTAsMCAzLDAgOSwyMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTEtNiI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgOSwwIiBjbGFzcz0iczExIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXgtNiI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgNiwxMCAzLDAiIGNsYXNzPSJzMTEiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0yMCwxNSAtNSw1IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMTAgMTAsMjAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCw1IDgsMTciIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwwIDcsMTMiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xNSwwIDcsOCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTEwLDAgOSwxIiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9InZtZC02Ij48cGF0aCBkPSJtMCwwIDAsMjAgMjAsMCBDIDEwLDIwIDcsMTAgMywwIiBjbGFzcz0iczExIi8+PHBhdGggZD0ibTAsMCAzLDAgYyA0LDEwIDcsMjAgMTcsMjAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm11LTYiPjxwYXRoIGQ9Im0wLDAgMCwyMCAzLDAgQyA3LDEwIDEwLDAgMjAsMCIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDcsMTAgMTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm16LTYiPjxwYXRoIGQ9Ik0wLDAgMywwIEMgMTAsMTAgMTUsMTAgMjAsMTAgMTUsMTAgMTAsMTAgMywyMCBMIDAsMjAiIGNsYXNzPSJzMTEiLz48cGF0aCBkPSJtMCwwIDMsMCBjIDcsMTAgMTIsMTAgMTcsMTAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDEwLDEwIDE1LDEwIDIwLDEwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IjBtdi03Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgMywyMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0iTTMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMW12LTciPjxwYXRoIGQ9Ik0yLjg3NSwwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ4bXYtNyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCw1IDMuNSwxLjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDEwIDQuNSw1LjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDYsOSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgNCwxNiIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJkbXYtNyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDMsMjAgeiIgY2xhc3M9InMxMiIvPjxwYXRoIGQ9Ik0zLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InVtdi03Ij48cGF0aCBkPSJNMywwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ6bXYtNyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0ibTYsMTAgMywxMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwxMCA2LDEwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZ2di03Ij48cGF0aCBkPSJNMjAsMjAgMCwyMCAwLDAgMjAsMCIgY2xhc3M9InMxMiIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTAtNyI+PHBhdGggZD0iTTAsMjAgMCwwIDMsMCA5LDIwIiBjbGFzcz0iczEyIi8+PHBhdGggZD0iTTAsMCAzLDAgOSwyMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTEtNyI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgOSwwIiBjbGFzcz0iczEyIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXgtNyI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgNiwxMCAzLDAiIGNsYXNzPSJzMTIiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0yMCwxNSAtNSw1IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMTAgMTAsMjAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCw1IDgsMTciIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwwIDcsMTMiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xNSwwIDcsOCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTEwLDAgOSwxIiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9InZtZC03Ij48cGF0aCBkPSJtMCwwIDAsMjAgMjAsMCBDIDEwLDIwIDcsMTAgMywwIiBjbGFzcz0iczEyIi8+PHBhdGggZD0ibTAsMCAzLDAgYyA0LDEwIDcsMjAgMTcsMjAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm11LTciPjxwYXRoIGQ9Im0wLDAgMCwyMCAzLDAgQyA3LDEwIDEwLDAgMjAsMCIgY2xhc3M9InMxMiIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDcsMTAgMTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm16LTciPjxwYXRoIGQ9Ik0wLDAgMywwIEMgMTAsMTAgMTUsMTAgMjAsMTAgMTUsMTAgMTAsMTAgMywyMCBMIDAsMjAiIGNsYXNzPSJzMTIiLz48cGF0aCBkPSJtMCwwIDMsMCBjIDcsMTAgMTIsMTAgMTcsMTAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDEwLDEwIDE1LDEwIDIwLDEwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IjBtdi04Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgMywyMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0iTTMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMW12LTgiPjxwYXRoIGQ9Ik0yLjg3NSwwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ4bXYtOCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCw1IDMuNSwxLjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDEwIDQuNSw1LjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDYsOSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgNCwxNiIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJkbXYtOCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDMsMjAgeiIgY2xhc3M9InMxMyIvPjxwYXRoIGQ9Ik0zLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InVtdi04Ij48cGF0aCBkPSJNMywwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ6bXYtOCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0ibTYsMTAgMywxMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwxMCA2LDEwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZ2di04Ij48cGF0aCBkPSJNMjAsMjAgMCwyMCAwLDAgMjAsMCIgY2xhc3M9InMxMyIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTAtOCI+PHBhdGggZD0iTTAsMjAgMCwwIDMsMCA5LDIwIiBjbGFzcz0iczEzIi8+PHBhdGggZD0iTTAsMCAzLDAgOSwyMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTEtOCI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgOSwwIiBjbGFzcz0iczEzIi8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXgtOCI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgNiwxMCAzLDAiIGNsYXNzPSJzMTMiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0yMCwxNSAtNSw1IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMTAgMTAsMjAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCw1IDgsMTciIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwwIDcsMTMiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xNSwwIDcsOCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTEwLDAgOSwxIiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9InZtZC04Ij48cGF0aCBkPSJtMCwwIDAsMjAgMjAsMCBDIDEwLDIwIDcsMTAgMywwIiBjbGFzcz0iczEzIi8+PHBhdGggZD0ibTAsMCAzLDAgYyA0LDEwIDcsMjAgMTcsMjAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm11LTgiPjxwYXRoIGQ9Im0wLDAgMCwyMCAzLDAgQyA3LDEwIDEwLDAgMjAsMCIgY2xhc3M9InMxMyIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDcsMTAgMTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm16LTgiPjxwYXRoIGQ9Ik0wLDAgMywwIEMgMTAsMTAgMTUsMTAgMjAsMTAgMTUsMTAgMTAsMTAgMywyMCBMIDAsMjAiIGNsYXNzPSJzMTMiLz48cGF0aCBkPSJtMCwwIDMsMCBjIDcsMTAgMTIsMTAgMTcsMTAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDEwLDEwIDE1LDEwIDIwLDEwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9IjBtdi05Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgMywyMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0iTTMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iMW12LTkiPjxwYXRoIGQ9Ik0yLjg3NSwwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ4bXYtOSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCw1IDMuNSwxLjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDEwIDQuNSw1LjUiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0wLDE1IDYsOSIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTAsMjAgNCwxNiIgY2xhc3M9InMyIi8+PC9nPjxnIGlkPSJkbXYtOSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDMsMjAgeiIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Ik0zLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJtMCwyMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InVtdi05Ij48cGF0aCBkPSJNMywwIDIwLDAgMjAsMjAgOSwyMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0ibTMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ6bXYtOSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0ibTYsMTAgMywxMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwxMCA2LDEwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZ2di05Ij48cGF0aCBkPSJNMjAsMjAgMCwyMCAwLDAgMjAsMCIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTAtOSI+PHBhdGggZD0iTTAsMjAgMCwwIDMsMCA5LDIwIiBjbGFzcz0iczE0Ii8+PHBhdGggZD0iTTAsMCAzLDAgOSwyMCIgY2xhc3M9InMxIi8+PHBhdGggZD0ibTAsMjAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bTEtOSI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgOSwwIiBjbGFzcz0iczE0Ii8+PHBhdGggZD0iTTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXgtOSI+PHBhdGggZD0iTTAsMCAwLDIwIDMsMjAgNiwxMCAzLDAiIGNsYXNzPSJzMTQiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0yMCwxNSAtNSw1IiBjbGFzcz0iczIiLz48cGF0aCBkPSJNMjAsMTAgMTAsMjAiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCw1IDgsMTciIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0yMCwwIDcsMTMiIGNsYXNzPSJzMiIvPjxwYXRoIGQ9Ik0xNSwwIDcsOCIgY2xhc3M9InMyIi8+PHBhdGggZD0iTTEwLDAgOSwxIiBjbGFzcz0iczIiLz48L2c+PGcgaWQ9InZtZC05Ij48cGF0aCBkPSJtMCwwIDAsMjAgMjAsMCBDIDEwLDIwIDcsMTAgMywwIiBjbGFzcz0iczE0Ii8+PHBhdGggZD0ibTAsMCAzLDAgYyA0LDEwIDcsMjAgMTcsMjAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm11LTkiPjxwYXRoIGQ9Im0wLDAgMCwyMCAzLDAgQyA3LDEwIDEwLDAgMjAsMCIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDcsMTAgMTAsMCAyMCwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm16LTkiPjxwYXRoIGQ9Ik0wLDAgMywwIEMgMTAsMTAgMTUsMTAgMjAsMTAgMTUsMTAgMTAsMTAgMywyMCBMIDAsMjAiIGNsYXNzPSJzMTQiLz48cGF0aCBkPSJtMCwwIDMsMCBjIDcsMTAgMTIsMTAgMTcsMTAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Im0wLDIwIDMsMCBDIDEwLDEwIDE1LDEwIDIwLDEwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi0yLTIiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi0zLTIiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi00LTIiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InM5Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi01LTIiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMCIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNi0yIj48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTEiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTctMiI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczciLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi04LTIiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMyIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtOS0yIj48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTQiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTItMyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczciLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTMtMyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTQtMyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczkiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTUtMyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi02LTMiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM4Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNy0zIj48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzOCIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTIiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTgtMyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi05LTMiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM4Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtMi00Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtMy00Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzOCIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNC00Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNS00Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTAiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTYtNCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczkiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi03LTQiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InM5Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMiIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtOC00Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTMiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTktNCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczkiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi0yLTUiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMCIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtMy01Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTAiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTQtNSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InM5Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi01LTUiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMCIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTAiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTYtNSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNy01Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTAiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi04LTUiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMCIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTMiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTktNSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtMi02Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTEiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczciLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTMtNiI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi00LTYiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNS02Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTEiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi02LTYiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTEiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTctNiI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMiIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtOC02Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTEiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi05LTYiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTQiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTItNyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InM3Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi0zLTciPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMiIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzOCIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNC03Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTIiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczkiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTUtNyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMCIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNi03Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTIiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczExIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi03LTciPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMiIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTIiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTgtNyI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMyIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtOS03Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTIiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi0yLTgiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMyIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzNyIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtMy04Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTMiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczgiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTQtOCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InM5Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi01LTgiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMyIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTAiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTYtOCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMSIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNy04Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTMiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEyIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi04LTgiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxMyIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTMiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTktOCI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtMi05Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTQiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczciLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTMtOSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InM4Ii8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi00LTkiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzOSIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtNS05Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTQiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEwIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi02LTkiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTEiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0idm12LTctOSI+PHBhdGggZD0iTTksMCAyMCwwIDIwLDIwIDksMjAgNiwxMCB6IiBjbGFzcz0iczE0Ii8+PHBhdGggZD0iTTMsMCAwLDAgMCwyMCAzLDIwIDYsMTAgeiIgY2xhc3M9InMxMiIvPjxwYXRoIGQ9Im0wLDAgMywwIDYsMjAgMTEsMCIgY2xhc3M9InMxIi8+PHBhdGggZD0iTTAsMjAgMywyMCA5LDAgMjAsMCIgY2xhc3M9InMxIi8+PC9nPjxnIGlkPSJ2bXYtOC05Ij48cGF0aCBkPSJNOSwwIDIwLDAgMjAsMjAgOSwyMCA2LDEwIHoiIGNsYXNzPSJzMTQiLz48cGF0aCBkPSJNMywwIDAsMCAwLDIwIDMsMjAgNiwxMCB6IiBjbGFzcz0iczEzIi8+PHBhdGggZD0ibTAsMCAzLDAgNiwyMCAxMSwwIiBjbGFzcz0iczEiLz48cGF0aCBkPSJNMCwyMCAzLDIwIDksMCAyMCwwIiBjbGFzcz0iczEiLz48L2c+PGcgaWQ9InZtdi05LTkiPjxwYXRoIGQ9Ik05LDAgMjAsMCAyMCwyMCA5LDIwIDYsMTAgeiIgY2xhc3M9InMxNCIvPjxwYXRoIGQ9Ik0zLDAgMCwwIDAsMjAgMywyMCA2LDEwIHoiIGNsYXNzPSJzMTQiLz48cGF0aCBkPSJtMCwwIDMsMCA2LDIwIDExLDAiIGNsYXNzPSJzMSIvPjxwYXRoIGQ9Ik0wLDIwIDMsMjAgOSwwIDIwLDAiIGNsYXNzPSJzMSIvPjwvZz48ZyBpZD0iYXJyb3cwIj48cGF0aCBkPSJtLTEyLC0zIDksMyAtOSwzIGMgMSwtMiAxLC00IDAsLTYgeiIgY2xhc3M9InMxNSIvPjxwYXRoIGQ9Ik0wLDAgLTE1LDAiIGNsYXNzPSJzMTYiLz48L2c+PG1hcmtlciBpZD0iYXJyb3doZWFkIiBzdHlsZT0iZmlsbDojMDA0MWM0IiBtYXJrZXJIZWlnaHQ9IjciIG1hcmtlcldpZHRoPSIxMCIgbWFya2VyVW5pdHM9InN0cm9rZVdpZHRoIiB2aWV3Qm94PSIwIC00IDExIDgiIHJlZlg9IjE1IiByZWZZPSIwIiBvcmllbnQ9ImF1dG8iPjxwYXRoIGQ9Ik0wIC00IDExIDAgMCA0eiIvPjwvbWFya2VyPjxtYXJrZXIgaWQ9ImFycm93dGFpbCIgc3R5bGU9ImZpbGw6IzAwNDFjNCIgbWFya2VySGVpZ2h0PSI3IiBtYXJrZXJXaWR0aD0iMTAiIG1hcmtlclVuaXRzPSJzdHJva2VXaWR0aCIgdmlld0JveD0iLTExIC00IDExIDgiIHJlZlg9Ii0xNSIgcmVmWT0iMCIgb3JpZW50PSJhdXRvIj48cGF0aCBkPSJNMCAtNCAtMTEgMCAwIDR6Ii8+PC9tYXJrZXI+PG1hcmtlciBpZD0idGVlIiBzdHlsZT0iZmlsbDojMDA0MWM0IiBtYXJrZXJIZWlnaHQ9IjYiIG1hcmtlcldpZHRoPSIxIiBtYXJrZXJVbml0cz0ic3Ryb2tlV2lkdGgiIHZpZXdCb3g9IjAgMCAxIDYiIHJlZlg9IjAiIHJlZlk9IjMiIG9yaWVudD0iYXV0byI+PHBhdGggZD0iTSAwIDAgTCAwIDYiIHN0eWxlPSJzdHJva2U6IzAwNDFjNDtzdHJva2Utd2lkdGg6MiIvPjwvbWFya2VyPjwvZGVmcz48ZyBpZD0id2F2ZXNfMCI+PHJlY3Qgd2lkdGg9IjU0MCIgaGVpZ2h0PSIxNTAiIHN0eWxlPSJzdHJva2U6bm9uZTtmaWxsOndoaXRlIi8+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIwLjUsMC41KSIgaWQ9ImxhbmVzXzAiPjxnIGlkPSJnbWFya3NfMCI+PGcgc3R5bGU9InN0cm9rZTojODg4O3N0cm9rZS13aWR0aDowLjU7c3Ryb2tlLWRhc2hhcnJheToxLDMiPjxsaW5lIGlkPSJnbWFya18wXzAiIHgxPSIwIiB5MT0iMCIgeDI9IjAiIHkyPSIxNTAiLz48bGluZSBpZD0iZ21hcmtfMV8wIiB4MT0iNDAiIHkxPSIwIiB4Mj0iNDAiIHkyPSIxNTAiLz48bGluZSBpZD0iZ21hcmtfMl8wIiB4MT0iODAiIHkxPSIwIiB4Mj0iODAiIHkyPSIxNTAiLz48bGluZSBpZD0iZ21hcmtfM18wIiB4MT0iMTIwIiB5MT0iMCIgeDI9IjEyMCIgeTI9IjE1MCIvPjxsaW5lIGlkPSJnbWFya180XzAiIHgxPSIxNjAiIHkxPSIwIiB4Mj0iMTYwIiB5Mj0iMTUwIi8+PGxpbmUgaWQ9ImdtYXJrXzVfMCIgeDE9IjIwMCIgeTE9IjAiIHgyPSIyMDAiIHkyPSIxNTAiLz48bGluZSBpZD0iZ21hcmtfNl8wIiB4MT0iMjQwIiB5MT0iMCIgeDI9IjI0MCIgeTI9IjE1MCIvPjxsaW5lIGlkPSJnbWFya183XzAiIHgxPSIyODAiIHkxPSIwIiB4Mj0iMjgwIiB5Mj0iMTUwIi8+PGxpbmUgaWQ9ImdtYXJrXzhfMCIgeDE9IjMyMCIgeTE9IjAiIHgyPSIzMjAiIHkyPSIxNTAiLz48bGluZSBpZD0iZ21hcmtfOV8wIiB4MT0iMzYwIiB5MT0iMCIgeDI9IjM2MCIgeTI9IjE1MCIvPjxsaW5lIGlkPSJnbWFya18xMF8wIiB4MT0iNDAwIiB5MT0iMCIgeDI9IjQwMCIgeTI9IjE1MCIvPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCw1KSIgaWQ9IndhdmVsYW5lXzBfMCI+PHRleHQgeD0iLTEwIiB5PSIxNSIgY2xhc3M9ImluZm8iIHRleHQtYW5jaG9yPSJlbmQiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3Bhbj5jbGs8L3RzcGFuPjwvdGV4dD48ZyBpZD0id2F2ZWxhbmVfZHJhd18wXzAiPjx1c2UgeGxpbms6aHJlZj0iI3BjbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwKSIgeGxpbms6aHJlZj0iI25jbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQwKSIgeGxpbms6aHJlZj0iI3BjbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYwKSIgeGxpbms6aHJlZj0iI25jbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwKSIgeGxpbms6aHJlZj0iI3BjbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCkiIHhsaW5rOmhyZWY9IiNuY2xrIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxMjApIiB4bGluazpocmVmPSIjcGNsayIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQwKSIgeGxpbms6aHJlZj0iI25jbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE2MCkiIHhsaW5rOmhyZWY9IiNwY2xrIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxODApIiB4bGluazpocmVmPSIjbmNsayIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjAwKSIgeGxpbms6aHJlZj0iI3BjbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMCkiIHhsaW5rOmhyZWY9IiNuY2xrIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNDApIiB4bGluazpocmVmPSIjcGNsayIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYwKSIgeGxpbms6aHJlZj0iI25jbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI4MCkiIHhsaW5rOmhyZWY9IiNwY2xrIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMDApIiB4bGluazpocmVmPSIjbmNsayIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzIwKSIgeGxpbms6aHJlZj0iI3BjbGsiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM0MCkiIHhsaW5rOmhyZWY9IiNuY2xrIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzNjApIiB4bGluazpocmVmPSIjcGNsayIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzgwKSIgeGxpbms6aHJlZj0iI25jbGsiLz48L2c+PC9nPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMzUpIiBpZD0id2F2ZWxhbmVfMV8wIj48dGV4dCB4PSItMTAiIHk9IjE1IiBjbGFzcz0iaW5mbyIgdGV4dC1hbmNob3I9ImVuZCIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuPkRhdGE8L3RzcGFuPjwvdGV4dD48ZyBpZD0id2F2ZWxhbmVfZHJhd18xXzAiPjx1c2UgeGxpbms6aHJlZj0iI3h4eCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjApIiB4bGluazpocmVmPSIjeHh4Ii8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSg0MCkiIHhsaW5rOmhyZWY9IiN4eHgiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDYwKSIgeGxpbms6aHJlZj0iI3h4eCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoODApIiB4bGluazpocmVmPSIjeG12LTMiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCkiIHhsaW5rOmhyZWY9IiN2dnYtMyIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIwKSIgeGxpbms6aHJlZj0iI3Ztdi0zLTQiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE0MCkiIHhsaW5rOmhyZWY9IiN2dnYtNCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTYwKSIgeGxpbms6aHJlZj0iI3Ztdi00LTUiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE4MCkiIHhsaW5rOmhyZWY9IiN2dnYtNSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjAwKSIgeGxpbms6aHJlZj0iI3ZteC01Ii8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMjApIiB4bGluazpocmVmPSIjeHh4Ii8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNDApIiB4bGluazpocmVmPSIjeHh4Ii8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNjApIiB4bGluazpocmVmPSIjeHh4Ii8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyODApIiB4bGluazpocmVmPSIjeG12LTIiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMCkiIHhsaW5rOmhyZWY9IiN2dnYtMiIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzIwKSIgeGxpbms6aHJlZj0iI3Z2di0yIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzNDApIiB4bGluazpocmVmPSIjdnZ2LTIiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM2MCkiIHhsaW5rOmhyZWY9IiN2bXgtMiIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzgwKSIgeGxpbms6aHJlZj0iI3h4eCIvPjx0ZXh0IHg9IjEwNiIgeT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3Bhbj5oZWFkPC90c3Bhbj48L3RleHQ+PHRleHQgeD0iMTQ2IiB5PSIxNSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+PHRzcGFuPmJvZHk8L3RzcGFuPjwvdGV4dD48dGV4dCB4PSIxODYiIHk9IjE1IiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4+dGFpbDwvdHNwYW4+PC90ZXh0Pjx0ZXh0IHg9IjMyNiIgeT0iMTUiIHRleHQtYW5jaG9yPSJtaWRkbGUiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3Bhbj5kYXRhPC90c3Bhbj48L3RleHQ+PC9nPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLDY1KSIgaWQ9IndhdmVsYW5lXzJfMCI+PHRleHQgeD0iLTEwIiB5PSIxNSIgY2xhc3M9ImluZm8iIHRleHQtYW5jaG9yPSJlbmQiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3Bhbj5SZXF1ZXN0PC90c3Bhbj48L3RleHQ+PGcgaWQ9IndhdmVsYW5lX2RyYXdfMl8wIj48dXNlIHhsaW5rOmhyZWY9IiMwMDAiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwKSIgeGxpbms6aHJlZj0iIzAwMCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNDApIiB4bGluazpocmVmPSIjMDAwIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSg2MCkiIHhsaW5rOmhyZWY9IiMwMDAiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDgwKSIgeGxpbms6aHJlZj0iIzBtMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTAwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTIwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTQwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTYwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTgwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjAwKSIgeGxpbms6aHJlZj0iIzFtMCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjIwKSIgeGxpbms6aHJlZj0iIzAwMCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjQwKSIgeGxpbms6aHJlZj0iIzAwMCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYwKSIgeGxpbms6aHJlZj0iIzAwMCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjgwKSIgeGxpbms6aHJlZj0iIzBtMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzAwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzIwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzQwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzYwKSIgeGxpbms6aHJlZj0iIzFtMCIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzgwKSIgeGxpbms6aHJlZj0iIzAwMCIvPjwvZz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCw5NSkiIGlkPSJ3YXZlbGFuZV8zXzAiPjx0ZXh0IHg9Ii0xMCIgeT0iMTUiIGNsYXNzPSJpbmZvIiB0ZXh0LWFuY2hvcj0iZW5kIiB4bWw6c3BhY2U9InByZXNlcnZlIj48dHNwYW4+PC90c3Bhbj48L3RleHQ+PGcgaWQ9IndhdmVsYW5lX2RyYXdfM18wIi8+PC9nPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMTI1KSIgaWQ9IndhdmVsYW5lXzRfMCI+PHRleHQgeD0iLTEwIiB5PSIxNSIgY2xhc3M9ImluZm8iIHRleHQtYW5jaG9yPSJlbmQiIHhtbDpzcGFjZT0icHJlc2VydmUiPjx0c3Bhbj5BY2tub3dsZWRnZTwvdHNwYW4+PC90ZXh0PjxnIGlkPSJ3YXZlbGFuZV9kcmF3XzRfMCI+PHVzZSB4bGluazpocmVmPSIjMTExIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyMCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDQwKSIgeGxpbms6aHJlZj0iIzExMSIvPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoNjApIiB4bGluazpocmVmPSIjMTExIi8+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSg4MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEwMCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyMCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE0MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE2MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDE4MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIwMCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDIyMCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI0MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI4MCkiIHhsaW5rOmhyZWY9IiMxbTAiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMwMCkiIHhsaW5rOmhyZWY9IiMwMDAiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDMyMCkiIHhsaW5rOmhyZWY9IiMwbTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM0MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM2MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDM4MCkiIHhsaW5rOmhyZWY9IiMxMTEiLz48L2c+PC9nPjxnIGlkPSJ3YXZlYXJjc18wIi8+PGcgaWQ9IndhdmVnYXBzXzAiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsNSkiIGlkPSJ3YXZlZ2FwXzBfMCI+PHVzZSB0cmFuc2Zvcm09InRyYW5zbGF0ZSgyNjApIiB4bGluazpocmVmPSIjZ2FwIi8+PC9nPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsMzUpIiBpZD0id2F2ZWdhcF8xXzAiPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYwKSIgeGxpbms6aHJlZj0iI2dhcCIvPjwvZz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLDY1KSIgaWQ9IndhdmVnYXBfMl8wIj48dXNlIHRyYW5zZm9ybT0idHJhbnNsYXRlKDI2MCkiIHhsaW5rOmhyZWY9IiNnYXAiLz48L2c+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwxMjUpIiBpZD0id2F2ZWdhcF80XzAiPjx1c2UgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjYwKSIgeGxpbms6aHJlZj0iI2dhcCIvPjwvZz48L2c+PGcvPjwvZz48ZyBpZD0iZ3JvdXBzXzAiPjxnLz48L2c+PC9nPjwvc3ZnPgo='><p>我的流程图:</p><img class="kroki" src='data:image/svg+xml;base64,RXJyb3IgNDAwOiBTeW50YXggRXJyb3I/IChsaW5lOiAxMik='><p>时序图:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br></pre></td><td class="code"><pre><span class="line">@startuml</span><br><span class="line">skinparam backgroundColor #FEFEFE</span><br><span class="line">skinparam sequenceArrowThickness 2</span><br><span class="line">skinparam roundcorner 20</span><br><span class="line">skinparam maxmessagesize 60</span><br><span class="line"></span><br><span class="line">actor Developer</span><br><span class="line">participant "GitHub" as GH</span><br><span class="line">box "GitHub Actions" #LightBlue</span><br><span class="line"> participant "build_env" as BE</span><br><span class="line"> participant "build_app" as BA</span><br><span class="line"> participant "release" as R</span><br><span class="line">end box</span><br><span class="line"></span><br><span class="line">Developer -> GH : 推送 v* 标签或手动触发</span><br><span class="line">activate GH</span><br><span class="line"></span><br><span class="line">GH -> BE : 触发工作流</span><br><span class="line">activate BE</span><br><span class="line"></span><br><span class="line">BE -> BE : 生成版本号</span><br><span class="line">BE -> BE : 更新 pubspec.yaml</span><br><span class="line">BE -> BE : 上传 pubspec.yaml</span><br><span class="line">BE --> BA : 完成环境准备</span><br><span class="line">deactivate BE</span><br><span class="line"></span><br><span class="line">activate BA</span><br><span class="line">BA -> BA : 下载 pubspec.yaml</span><br><span class="line">BA -> BA : 设置 Flutter</span><br><span class="line"></span><br><span class="line">par 并行构建</span><br><span class="line"> BA -> BA : 构建 Android</span><br><span class="line"> BA -> BA : 构建 Android AAB</span><br><span class="line"> BA -> BA : 构建 Web</span><br><span class="line"> BA -> BA : 构建 Linux</span><br><span class="line"> BA -> BA : 构建 Windows</span><br><span class="line"> BA -> BA : 构建 iOS</span><br><span class="line"> BA -> BA : 构建 macOS</span><br><span class="line">end</span><br><span class="line"></span><br><span class="line">BA -> BA : 压缩构建结果</span><br><span class="line">BA -> BA : 上传 artifacts</span><br><span class="line">BA --> R : 完成所有构建</span><br><span class="line">deactivate BA</span><br><span class="line"></span><br><span class="line">activate R</span><br><span class="line">R -> R : 下载所有 artifacts</span><br><span class="line">R -> R : 创建 GitHub Release</span><br><span class="line">R -> R : 上传 Release 资产</span><br><span class="line">R --> GH : 完成发布</span><br><span class="line">deactivate R</span><br><span class="line"></span><br><span class="line">GH --> Developer : 工作流完成通知</span><br><span class="line">deactivate GH</span><br><span class="line"></span><br><span class="line">@enduml</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><p><a href="https://plantuml.com/zh/">PlantUML</a>是一个通用性很强的工具,可以快速、直接地创建各种图表。用来画组件图、部署图、状态图、时序图、甘特图等UML以及非UML图。</p>
<p>线上版 <a href="https://</summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Hexo" scheme="https://xmaihh.github.io/blog/tags/Hexo/"/>
</entry>
<entry>
<title>用于构建和发布 Flutter 应用程序的GitHub Actions 工作流程</title>
<link href="https://xmaihh.github.io/blog/2024/07/26/yong-yu-gou-jian-he-fa-bu-flutter-ying-yong-cheng-xu-de-github-actions-gong-zuo-liu-cheng/"/>
<id>https://xmaihh.github.io/blog/2024/07/26/yong-yu-gou-jian-he-fa-bu-flutter-ying-yong-cheng-xu-de-github-actions-gong-zuo-liu-cheng/</id>
<published>2024-07-26T03:05:13.000Z</published>
<updated>2024-07-26T03:05:13.000Z</updated>
<content type="html"><![CDATA[<p>在这个Flutter项目 <a href="https://github.com/xmaihh/FlutterHub"><strong>https://github.com/xmaihh/FlutterHub</strong> </a> 中使用Github Actions自动化构建(Android、iOS、Web、Linux、Windows、macOS)应用并发布到Release。</p><figure class="highlight tap"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"> ┌─────────────────┐ ┌─────────────────┐</span><br><span class="line"> │ 推送 v* 标签 │ │ 手动触发 │</span><br><span class="line"> └────────┬────────┘ └────────┬────────┘</span><br><span class="line"> │ │</span><br><span class="line"> └──────────┬──────────┘</span><br><span class="line"> ▼</span><br><span class="line"> ┌───────────────────┐</span><br><span class="line"> │ build_env │</span><br><span class="line"> │ ───────────────── │</span><br><span class="line"> │ 生成版本号 │</span><br><span class="line"> │ 更新 pubspec.yaml │</span><br><span class="line"> │ 上传 pubspec.yaml │</span><br><span class="line"> └──────────┬────────┘</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line"> ┌───────────────────────────────────────────────────────┐</span><br><span class="line"> │ build_app │</span><br><span class="line"> │ ┌─────────┐ ┌─────────┐ ┌───┐ ┌─────┐ ┌───┐ ┌───┐ ┌───┐│</span><br><span class="line"> │ │ Android │ │Android │ │Web│ │Linux│ │Win│ │iOS│ │Mac││</span><br><span class="line"> │ │ │ │ AAB │ │ │ │ │ │ │ │ │ │ ││</span><br><span class="line"> │ │1.下载 │ │1.下载 │ │<span class="number"> 1 </span>│ │ <span class="number"> 1 </span> │ │<span class="number"> 1 </span>│ │<span class="number"> 1 </span>│ │<span class="number"> 1 </span>││</span><br><span class="line"> │ │2.设置 │ │2.设置 │ │<span class="number"> 2 </span>│ │ <span class="number"> 2 </span> │ │<span class="number"> 2 </span>│ │<span class="number"> 2 </span>│ │<span class="number"> 2 </span>││</span><br><span class="line"> │ │3.构建 │ │3.构建 │ │<span class="number"> 3 </span>│ │ <span class="number"> 3 </span> │ │<span class="number"> 3 </span>│ │<span class="number"> 3 </span>│ │<span class="number"> 3 </span>││</span><br><span class="line"> │ │4.压缩 │ │4.压缩 │ │<span class="number"> 4 </span>│ │ <span class="number"> 4 </span> │ │<span class="number"> 4 </span>│ │<span class="number"> 4 </span>│ │<span class="number"> 4 </span>││</span><br><span class="line"> │ │5.上传 │ │5.上传 │ │<span class="number"> 5 </span>│ │ <span class="number"> 5 </span> │ │<span class="number"> 5 </span>│ │<span class="number"> 5 </span>│ │<span class="number"> 5 </span>││</span><br><span class="line"> │ └─────────┘ └─────────┘ └───┘ └─────┘ └───┘ └───┘ └───┘│</span><br><span class="line"> └───────────────────────────┬───────────────────────────┘</span><br><span class="line"> │</span><br><span class="line"> ▼</span><br><span class="line"> ┌───────────────────┐</span><br><span class="line"> │ release │</span><br><span class="line"> │ ───────────────── │</span><br><span class="line"> │ 下载所有 artifacts │</span><br><span class="line"> │ 创建 GitHub Release│</span><br><span class="line"> │ 上传 Release 资产 │</span><br><span class="line"> └───────────────────┘</span><br><span class="line"></span><br><span class="line">图例:</span><br><span class="line">1 = 下载 pubspec.yaml <span class="number"> 2 </span>= 设置 Flutter <span class="number"> 3 </span>= 构建</span><br><span class="line">4 = 压缩 <span class="number"> 5 </span>= 上传 artifact</span><br></pre></td></tr></table></figure><p>我的PlantUML图片:</p><img class="kroki" src='data:image/svg+xml;base64,RXJyb3IgNDAwOiBTeW50YXggRXJyb3I/IChsaW5lOiAxMik='><p>工作流程的主要步骤:</p><h1 id="触发条件:"><a href="#触发条件:" class="headerlink" title="触发条件:"></a>触发条件:</h1><ul><li>当推送带有 “v” 开头的标签时</li><li>可以手动触发</li></ul><h1 id="工作流程包含三个主要任务:"><a href="#工作流程包含三个主要任务:" class="headerlink" title="工作流程包含三个主要任务:"></a>工作流程包含三个主要任务:</h1><ul><li>build_env:更新版本号</li><li>build_app:构建多平台应用</li><li>release:创建发布和上传构建产物</li></ul><ol><li>build_env 任务:</li></ol><ul><li>生成版本号</li><li>更新 pubspec.yaml 中的版本号</li><li>上传更新后的 pubspec.yaml 文件</li></ul><ol start="2"><li>build_app 任务:</li></ol><ul><li>使用矩阵策略为不同平台构建应用(Android、iOS、Web、Linux、Windows、macOS)</li><li>下载更新后的 pubspec.yaml</li><li>设置 Flutter 环境</li><li>安装依赖</li><li>根据平台执行相应的构建命令</li><li>压缩构建产物</li><li>上传构建产物作为 artifact</li></ul><ol start="3"><li>release 任务:</li></ol><ul><li>下载所有构建产物</li><li>创建 GitHub Release</li><li>将构建产物上传为 Release 资产</li></ul><p>在你的<strong>Flutter项目仓库</strong>根目录下创建一个 <strong>.github/workflows</strong> 文件夹,在该文件夹内创建一个新的 <strong>YAML</strong> 文件(例如 deploy.yml)用于定义 <strong>GitHub Actions</strong> 工作流。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br><span class="line">214</span><br><span class="line">215</span><br><span class="line">216</span><br><span class="line">217</span><br><span class="line">218</span><br><span class="line">219</span><br><span class="line">220</span><br><span class="line">221</span><br><span class="line">222</span><br><span class="line">223</span><br><span class="line">224</span><br><span class="line">225</span><br><span class="line">226</span><br><span class="line">227</span><br><span class="line">228</span><br><span class="line">229</span><br><span class="line">230</span><br><span class="line">231</span><br><span class="line">232</span><br><span class="line">233</span><br><span class="line">234</span><br><span class="line">235</span><br><span class="line">236</span><br><span class="line">237</span><br><span class="line">238</span><br><span class="line">239</span><br><span class="line">240</span><br><span class="line">241</span><br><span class="line">242</span><br><span class="line">243</span><br><span class="line">244</span><br><span class="line">245</span><br><span class="line">246</span><br><span class="line">247</span><br><span class="line">248</span><br><span class="line">249</span><br><span class="line">250</span><br><span class="line">251</span><br><span class="line">252</span><br><span class="line">253</span><br><span class="line">254</span><br><span class="line">255</span><br><span class="line">256</span><br><span class="line">257</span><br><span class="line">258</span><br><span class="line">259</span><br><span class="line">260</span><br><span class="line">261</span><br><span class="line">262</span><br><span class="line">263</span><br><span class="line">264</span><br><span class="line">265</span><br><span class="line">266</span><br><span class="line">267</span><br><span class="line">268</span><br><span class="line">269</span><br><span class="line">270</span><br><span class="line">271</span><br><span class="line">272</span><br><span class="line">273</span><br><span class="line">274</span><br><span class="line">275</span><br><span class="line">276</span><br><span class="line">277</span><br><span class="line">278</span><br><span class="line">279</span><br><span class="line">280</span><br><span class="line">281</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">name:</span> <span class="string">Flutter</span> <span class="string">Release</span> <span class="string">Build</span> <span class="string">CI/CD</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># 触发条件配置</span></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line"> <span class="attr">push:</span></span><br><span class="line"> <span class="attr">tags:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">'v*'</span> <span class="comment"># 当推送的标签以"v"开头时触发,常用于版本发布</span></span><br><span class="line"> <span class="attr">workflow_dispatch:</span> <span class="comment"># 允许手动触发工作流,便于管理和测试</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line"> <span class="comment"># 在 Ubuntu 环境中运行,避免了 macOS 的 sed 问题</span></span><br><span class="line"> <span class="comment"># 更新 pubspec.yaml 中的版本号</span></span><br><span class="line"> <span class="comment"># 将新版本号作为输出,供其他 job 使用</span></span><br><span class="line"> <span class="attr">build_env:</span> <span class="comment"># 更新pubspec.yaml中的版本号:</span></span><br><span class="line"> <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="attr">steps:</span></span><br><span class="line"> <span class="comment"># 检出代码</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">fetch-depth:</span> <span class="number">0</span> <span class="comment"># 获取全部历史,以便计算版本号</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 生成版本号</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Generate</span> <span class="string">Version</span> <span class="string">Number</span></span><br><span class="line"> <span class="attr">id:</span> <span class="string">generate_version</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> # 标签触发 (e.g. v1.0或1.0) 移除 refs/tags/ 前缀和可选的 'v' 前缀</span></span><br><span class="line"><span class="string"> if [[ ${{ github.ref }} == refs/tags/* ]]; then</span></span><br><span class="line"><span class="string"> TAG_VERSION=$(echo ${{ github.ref }} | sed -E 's/^refs\/tags\/(v)?//')</span></span><br><span class="line"><span class="string"> echo "This is a tag release: $TAG_VERSION"</span></span><br><span class="line"><span class="string"></span> </span><br><span class="line"> <span class="comment"># 分支触发 (e.g. main或feature/new-feature) 移除 refs/heads/ 前缀</span></span><br><span class="line"> <span class="string">elif</span> [[ <span class="string">$<span class="template-variable">{{ github.ref }}</span></span> <span class="string">==</span> <span class="string">refs/heads/*</span> ]]<span class="string">;</span> <span class="string">then</span></span><br><span class="line"> <span class="string">TAG_VERSION=$(echo</span> <span class="string">${{</span> <span class="string">github.ref</span> <span class="string">}}</span> <span class="string">|</span> <span class="string">sed</span> <span class="string">-E</span> <span class="string">'s/^refs\/heads\///'</span> <span class="string">|</span> <span class="string">sed</span> <span class="string">'s/\//-/g'</span><span class="string">)</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"This is a branch push: $TAG_VERSION"</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"># Pull Request触发 (e.g. pr-123) 提取PR的编号 pr-<number></span></span><br><span class="line"> <span class="string">elif</span> [[ <span class="string">$<span class="template-variable">{{ github.ref }}</span></span> <span class="string">==</span> <span class="string">refs/pull/*</span> ]]<span class="string">;</span> <span class="string">then</span></span><br><span class="line"> <span class="string">PR_NUMBER=$(echo</span> <span class="string">${{</span> <span class="string">github.ref</span> <span class="string">}}</span> <span class="string">|</span> <span class="string">sed</span> <span class="string">-E</span> <span class="string">'s/^refs\/pull\/([0-9]+)\/merge$/\1/'</span><span class="string">)</span></span><br><span class="line"> <span class="string">TAG_VERSION="pr-$PR_NUMBER"</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"This is a Pull Request: $TAG_VERSION"</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 其他情况:直接使用 github.ref 的值 包含任何 "/"会被替换为 "-"</span></span><br><span class="line"> <span class="string">else</span></span><br><span class="line"> <span class="string">TAG_VERSION=$(echo</span> <span class="string">${{</span> <span class="string">github.ref</span> <span class="string">}}</span> <span class="string">|</span> <span class="string">sed</span> <span class="string">'s/\//-/g'</span><span class="string">)</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"This is another trigger: $TAG_VERSION"</span></span><br><span class="line"> <span class="string">fi</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"TAG_VERSION=$TAG_VERSION"</span> </span><br><span class="line"> <span class="string">COMMIT_COUNT=$(git</span> <span class="string">rev-list</span> <span class="string">--count</span> <span class="string">HEAD)</span> <span class="comment"># 计算提交数量</span></span><br><span class="line"> <span class="string">SHORT_HASH=$(git</span> <span class="string">rev-parse</span> <span class="string">--short</span> <span class="string">HEAD)</span> <span class="comment"># 获取最近一次提交的短哈希</span></span><br><span class="line"> <span class="string">BUILD_VERSION="${COMMIT_COUNT}"</span> <span class="comment"># 组合成完整的构建版本号</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"BUILD_VERSION=${BUILD_VERSION}"</span> <span class="string">>></span> <span class="string">$GITHUB_OUTPUT</span> <span class="comment"># 设置输出变量</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"Generated BUILD_VERSION: ${BUILD_VERSION}"</span> <span class="comment"># 打印完整的构建版本号 (e.g. 34)</span></span><br><span class="line"> <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 更新pubspec.yaml中的版本号</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Update</span> <span class="string">version</span> <span class="string">in</span> <span class="string">pubspec.yaml</span></span><br><span class="line"> <span class="attr">id:</span> <span class="string">update_version_in_pubspec</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> # 从pubspec.yaml中提取主版本号,(e.g. version: 1.0.0+1,提取1.0.0)</span></span><br><span class="line"><span class="string"> MAIN_VERSION=$(grep "^version:" pubspec.yaml | sed -E 's/version: ([0-9]+\.[0-9]+\.[0-9]+).*/\1/')</span></span><br><span class="line"><span class="string"> echo "MAIN_VERSION=${MAIN_VERSION}"</span></span><br><span class="line"><span class="string"></span> </span><br><span class="line"> <span class="comment"># 组合新的完整版本号 (e.g. 1.0.0+34)</span></span><br><span class="line"> <span class="string">FULL_VERSION="${MAIN_VERSION}+${{</span> <span class="string">steps.generate_version.outputs.BUILD_VERSION</span> <span class="string">}}"</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"FULL_VERSION=${FULL_VERSION}"</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 更新新版本号到pubspec.yaml文件</span></span><br><span class="line"> <span class="string">sed</span> <span class="string">-i</span> <span class="string">"s/^version: .*/version: ${FULL_VERSION}/"</span> <span class="string">pubspec.yaml</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"Updated pubspec.yaml content:"</span></span><br><span class="line"> <span class="string">cat</span> <span class="string">pubspec.yaml</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 验证更新</span></span><br><span class="line"> <span class="string">if</span> <span class="string">grep</span> <span class="string">-q</span> <span class="string">"^version: ${FULL_VERSION}"</span> <span class="string">pubspec.yaml;</span> <span class="string">then</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"Version updated successfully to ${FULL_VERSION}"</span></span><br><span class="line"> <span class="string">else</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"Failed to update version"</span></span><br><span class="line"> <span class="string">echo</span> <span class="string">"Current version line:"</span></span><br><span class="line"> <span class="string">grep</span> <span class="string">"^version:"</span> <span class="string">pubspec.yaml</span></span><br><span class="line"> <span class="string">exit</span> <span class="number">1</span></span><br><span class="line"> <span class="string">fi</span></span><br><span class="line"> </span><br><span class="line"> <span class="string">echo</span> <span class="string">"FULL_VERSION=${FULL_VERSION}"</span> <span class="string">></span> <span class="string">_build_env.config</span></span><br><span class="line"> <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">updated</span> <span class="string">pubspec.yaml</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">build_env_files</span></span><br><span class="line"> <span class="attr">path:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> pubspec.yaml</span></span><br><span class="line"><span class="string"> _build_env.config</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"> <span class="attr">build_app:</span></span><br><span class="line"> <span class="attr">needs:</span> <span class="string">build_env</span></span><br><span class="line"> <span class="attr">runs-on:</span> <span class="string">${{</span> <span class="string">matrix.os</span> <span class="string">}}</span> <span class="comment"># 使用矩阵策略中指定的操作系统进行构建</span></span><br><span class="line"> <span class="attr">strategy:</span></span><br><span class="line"> <span class="attr">matrix:</span></span><br><span class="line"> <span class="attr">include:</span></span><br><span class="line"> <span class="comment"># 定义不同的构建任务,每个任务对应一个平台</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">android</span></span><br><span class="line"> <span class="attr">os:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">android-aab</span></span><br><span class="line"> <span class="attr">os:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">web</span></span><br><span class="line"> <span class="attr">os:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">linux</span></span><br><span class="line"> <span class="attr">os:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">windows</span></span><br><span class="line"> <span class="attr">os:</span> <span class="string">windows-latest</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">ios</span></span><br><span class="line"> <span class="attr">os:</span> <span class="string">macos-latest</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">macos</span></span><br><span class="line"> <span class="attr">os:</span> <span class="string">macos-latest</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">steps:</span></span><br><span class="line"> <span class="comment"># 检出代码</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">uses:</span> <span class="string">actions/checkout@v4</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Download</span> <span class="string">updated</span> <span class="string">pubspec.yaml</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/download-artifact@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">build_env_files</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Replace</span> <span class="string">pubspec.yaml</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> ls</span></span><br><span class="line"><span class="string"> cat pubspec.yaml</span></span><br><span class="line"><span class="string"> pwd</span></span><br><span class="line"><span class="string"> cat _build_env.config</span></span><br><span class="line"><span class="string"></span> <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Read</span> <span class="string">FULL_VERSION</span> <span class="string">from</span> <span class="string">_build_env.config</span></span><br><span class="line"> <span class="attr">id:</span> <span class="string">read_build_env</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> echo "Reading version from _build_env.config..."</span></span><br><span class="line"><span class="string"> FULL_VERSION=$(grep 'FULL_VERSION=' _build_env.config | cut -d '=' -f2)</span></span><br><span class="line"><span class="string"> echo "FULL_VERSION=${FULL_VERSION}" >> $GITHUB_OUTPUT # 设置输出变量</span></span><br><span class="line"><span class="string"></span> <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 设置Flutter环境</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Set</span> <span class="string">up</span> <span class="string">Flutter</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">subosito/flutter-action@v2</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">channel:</span> <span class="string">stable</span> <span class="comment"># 使用Flutter的稳定版</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 显示Flutter版本信息</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Check</span> <span class="string">Flutter</span> <span class="string">version</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">flutter</span> <span class="string">--version</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 安装Flutter依赖</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">flutter</span> <span class="string">pub</span> <span class="string">get</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 根据矩阵配置,为不同平台执行构建命令</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">Android</span> <span class="string">APK</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.name</span> <span class="string">==</span> <span class="string">'android'</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> flutter build apk</span></span><br><span class="line"><span class="string"> flutter build apk --split-per-abi</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">Android</span> <span class="string">App</span> <span class="string">Bundle</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.name</span> <span class="string">==</span> <span class="string">'android-aab'</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">flutter</span> <span class="string">build</span> <span class="string">appbundle</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">Web</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.name</span> <span class="string">==</span> <span class="string">'web'</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">flutter</span> <span class="string">build</span> <span class="string">web</span> <span class="string">--base-href=/FlutterHub/</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">Linux</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.name</span> <span class="string">==</span> <span class="string">'linux'</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> sudo apt-get update -y</span></span><br><span class="line"><span class="string"> sudo apt-get install -y ninja-build libgtk-3-dev</span></span><br><span class="line"><span class="string"> flutter build linux</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">Windows</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.name</span> <span class="string">==</span> <span class="string">'windows'</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">flutter</span> <span class="string">build</span> <span class="string">windows</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">iOS</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.name</span> <span class="string">==</span> <span class="string">'ios'</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">flutter</span> <span class="string">build</span> <span class="string">ios</span> <span class="string">--release</span> <span class="string">--no-codesign</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">macOS</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.name</span> <span class="string">==</span> <span class="string">'macos'</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">flutter</span> <span class="string">build</span> <span class="string">macos</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 部署Web平台构建产物到GitHub Pages</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Deploy</span> <span class="string">to</span> <span class="string">GitHub</span> <span class="string">Pages</span></span><br><span class="line"> <span class="attr">if:</span> <span class="string">matrix.name</span> <span class="string">==</span> <span class="string">'web'</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">peaceiris/actions-gh-pages@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">github_token:</span> <span class="string">${{</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">publish_dir:</span> <span class="string">./build/web</span></span><br><span class="line"> <span class="attr">publish_branch:</span> <span class="string">gh-pages</span></span><br><span class="line"> <span class="attr">user_name:</span> <span class="string">'github-actions[bot]'</span></span><br><span class="line"> <span class="attr">user_email:</span> <span class="string">'github-actions[bot]@users.noreply.github.com'</span></span><br><span class="line"> <span class="attr">commit_message:</span> <span class="string">'Deploy to GitHub Pages: $<span class="template-variable">{{ steps.read_build_env.outputs.FULL_VERSION }}</span>'</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># 根据平台压缩构建产物,为上传做准备</span></span><br><span class="line"> <span class="comment"># 使用不同的命令和文件路径根据平台进行压缩</span></span><br><span class="line"> <span class="comment"># 这里使用了不同的shell命令和条件判断来处理不同的操作系统和构建产物</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Compress</span> <span class="string">Build</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> if [ "${{ matrix.name }}" = "android" ]; then</span></span><br><span class="line"><span class="string"> mv build/app/outputs/flutter-apk/app-release.apk ./app-release-all-${{ steps.read_build_env.outputs.FULL_VERSION }}.apk</span></span><br><span class="line"><span class="string"> mv build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk ./app-release-armeabi-v7a-${{ steps.read_build_env.outputs.FULL_VERSION }}.apk</span></span><br><span class="line"><span class="string"> mv build/app/outputs/flutter-apk/app-arm64-v8a-release.apk ./app-release-arm64-v8a-${{ steps.read_build_env.outputs.FULL_VERSION }}.apk</span></span><br><span class="line"><span class="string"> mv build/app/outputs/flutter-apk/app-x86_64-release.apk ./app-release-x86_64-${{ steps.read_build_env.outputs.FULL_VERSION }}.apk</span></span><br><span class="line"><span class="string"> elif [ "${{ matrix.name }}" = "android-aab" ]; then</span></span><br><span class="line"><span class="string"> mv build/app/outputs/bundle/release/app-release.aab ./app-release-${{ steps.read_build_env.outputs.FULL_VERSION }}.aab</span></span><br><span class="line"><span class="string"> elif [ "${{ matrix.name }}" = "web" ]; then</span></span><br><span class="line"><span class="string"> zip -r web-release-${{ steps.read_build_env.outputs.FULL_VERSION }}.zip build/web</span></span><br><span class="line"><span class="string"> elif [ "${{ matrix.name }}" = "linux" ]; then</span></span><br><span class="line"><span class="string"> zip -r linux-release-${{ steps.read_build_env.outputs.FULL_VERSION }}.zip build/linux/x64/release/bundle</span></span><br><span class="line"><span class="string"> elif [ "${{ matrix.name }}" = "windows" ]; then</span></span><br><span class="line"><span class="string"> powershell Compress-Archive build/windows/x64/runner/Release windows-release-${{ steps.read_build_env.outputs.FULL_VERSION }}.zip</span></span><br><span class="line"><span class="string"> elif [ "${{ matrix.name }}" = "ios" ]; then</span></span><br><span class="line"><span class="string"> mkdir Payload</span></span><br><span class="line"><span class="string"> cp -r build/ios/iphoneos/Runner.app Payload/</span></span><br><span class="line"><span class="string"> zip -r ios-release-${{ steps.read_build_env.outputs.FULL_VERSION }}.ipa Payload/</span></span><br><span class="line"><span class="string"> zip -r ios-release-${{ steps.read_build_env.outputs.FULL_VERSION }}.zip build/ios/iphoneos</span></span><br><span class="line"><span class="string"> elif [ "${{ matrix.name }}" = "macos" ]; then</span></span><br><span class="line"><span class="string"> zip -r macos-release-${{ steps.read_build_env.outputs.FULL_VERSION }}.zip build/macos/Build/Products/Release</span></span><br><span class="line"><span class="string"> fi</span></span><br><span class="line"><span class="string"></span> <span class="attr">shell:</span> <span class="string">bash</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">Build</span> <span class="string">Artifact</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/upload-artifact@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">name:</span> <span class="string">${{</span> <span class="string">matrix.name</span> <span class="string">}}-artifact</span></span><br><span class="line"> <span class="attr">path:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> *.apk</span></span><br><span class="line"><span class="string"> *.aab</span></span><br><span class="line"><span class="string"> *.ipa</span></span><br><span class="line"><span class="string"> *.zip</span></span><br><span class="line"><span class="string"></span> <span class="attr">if-no-files-found:</span> <span class="string">error</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">release:</span></span><br><span class="line"> <span class="attr">needs:</span> <span class="string">build_app</span></span><br><span class="line"> <span class="attr">runs-on:</span> <span class="string">ubuntu-latest</span></span><br><span class="line"> <span class="attr">steps:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Download</span> <span class="string">all</span> <span class="string">artifacts</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/download-artifact@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">path:</span> <span class="string">artifacts</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Display</span> <span class="string">structure</span> <span class="string">of</span> <span class="string">downloaded</span> <span class="string">files</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">ls</span> <span class="string">-R</span> <span class="string">artifacts</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Read</span> <span class="string">FULL_VERSION</span> <span class="string">from</span> <span class="string">_build_env.config</span></span><br><span class="line"> <span class="attr">id:</span> <span class="string">read_build_env</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> echo "Reading version from _build_env.config..."</span></span><br><span class="line"><span class="string"> FULL_VERSION=$(grep 'FULL_VERSION=' artifacts/build_env_files/_build_env.config | cut -d '=' -f2)</span></span><br><span class="line"><span class="string"> echo "FULL_VERSION=${FULL_VERSION}" >> $GITHUB_OUTPUT</span></span><br><span class="line"><span class="string"></span> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Create</span> <span class="string">Release</span></span><br><span class="line"> <span class="attr">id:</span> <span class="string">create_release</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/create-release@v1</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">GITHUB_TOKEN:</span> <span class="string">${{</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">tag_name:</span> <span class="string">${{</span> <span class="string">steps.read_build_env.outputs.FULL_VERSION</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">release_name:</span> <span class="string">Release</span> <span class="string">${{</span> <span class="string">steps.read_build_env.outputs.FULL_VERSION</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">draft:</span> <span class="literal">false</span></span><br><span class="line"> <span class="attr">prerelease:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">Release</span> <span class="string">Assets</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">GITHUB_TOKEN:</span> <span class="string">${{</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> for artifact in artifacts/*/*.{apk,aab,ipa,zip}</span></span><br><span class="line"><span class="string"> do</span></span><br><span class="line"><span class="string"> if [ -f "$artifact" ]; then</span></span><br><span class="line"><span class="string"> asset_name=$(basename "$artifact")</span></span><br><span class="line"><span class="string"> echo "Uploading $asset_name"</span></span><br><span class="line"><span class="string"> curl --fail -X POST \</span></span><br><span class="line"><span class="string"> -H "Authorization: token $GITHUB_TOKEN" \</span></span><br><span class="line"><span class="string"> -H "Content-Type: $(file -b --mime-type $artifact)" \</span></span><br><span class="line"><span class="string"> --data-binary @"$artifact" \</span></span><br><span class="line"><span class="string"> "https://uploads.github.com/repos/${{ github.repository }}/releases/${{ steps.create_release.outputs.id }}/assets?name=$asset_name"</span></span><br><span class="line"><span class="string"> fi</span></span><br><span class="line"><span class="string"> done</span></span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><p>在这个Flutter项目 <a href="https://github.com/xmaihh/FlutterHub"><strong>https://github.com/xmaihh/FlutterHub</strong> </a> 中使用Github Actions自</summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Flutter" scheme="https://xmaihh.github.io/blog/tags/Flutter/"/>
<category term="Github Actions" scheme="https://xmaihh.github.io/blog/tags/Github-Actions/"/>
<category term="CI/CD" scheme="https://xmaihh.github.io/blog/tags/CI-CD/"/>
</entry>
<entry>
<title>Flutter各个平台的构建产物</title>
<link href="https://xmaihh.github.io/blog/2024/07/25/flutter-ge-ge-ping-tai-de-gou-jian-chan-wu/"/>
<id>https://xmaihh.github.io/blog/2024/07/25/flutter-ge-ge-ping-tai-de-gou-jian-chan-wu/</id>
<published>2024-07-25T05:47:09.000Z</published>
<updated>2024-07-25T05:47:09.000Z</updated>
<content type="html"><![CDATA[<p>在 Flutter 中创建一个新项目,同时支持 Android、iOS、macOS、Windows 和 Linux 平台,假设你已经安装了 Flutter SDK 和必要的开发工具(如 Android Studio、Xcode 等)。</p><h1 id="安装-Flutter-SDK"><a href="#安装-Flutter-SDK" class="headerlink" title="安装 Flutter SDK"></a>安装 Flutter SDK</h1><p>按照 <a href="https://flutter.dev/docs/get-started/install"><strong>Flutter 官网</strong></a> 下载并按照指南进行安装。</p><h1 id="创建新项目"><a href="#创建新项目" class="headerlink" title="创建新项目"></a>创建新项目</h1><p>打开终端或命令提示符,使用以下命令创建一个新的 Flutter 项目:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter create my_flutter_app</span><br></pre></td></tr></table></figure><p>这个命令会创建一个名为 <code>my_flutter_app</code> 的新目录,其中包含一个支持 <strong>Android</strong> 和 <strong>iOS</strong> 的 Flutter 项目的初始模板。</p><h1 id="启用桌面支持"><a href="#启用桌面支持" class="headerlink" title="启用桌面支持"></a>启用桌面支持</h1><p>默认情况下,Flutter 支持 Android 和 iOS。如果你想要为 macOS、Windows 和 Linux 添加支持,你需要为每个桌面平台启用支持。运行以下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> my_flutter_app</span><br><span class="line">flutter config --enable-windows-desktop</span><br><span class="line">flutter config --enable-macos-desktop</span><br><span class="line">flutter config --enable-linux-desktop</span><br></pre></td></tr></table></figure><p>这些命令会启用桌面平台的支持。在你启用桌面支持之后,你的项目目录中将包括针对每个桌面平台的特定代码和资源。</p><h1 id="运行和构建应用"><a href="#运行和构建应用" class="headerlink" title="运行和构建应用"></a>运行和构建应用</h1><p>现在你可以尝试在各个平台上运行你的应用了。使用以下命令来运行对应平台的应用:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">flutter run -d windows <span class="comment"># 运行 Windows 应用</span></span><br><span class="line">flutter run -d macos <span class="comment"># 运行 macOS 应用</span></span><br><span class="line">flutter run -d linux <span class="comment"># 运行 Linux 应用</span></span><br><span class="line">flutter run -d android <span class="comment"># 运行 Android 应用</span></span><br><span class="line">flutter run -d ios <span class="comment"># 运行 iOS 应用</span></span><br></pre></td></tr></table></figure><h1 id="各个平台的构建产物"><a href="#各个平台的构建产物" class="headerlink" title="各个平台的构建产物"></a>各个平台的构建产物</h1><p><code>flutter build</code> 命令是用于构建和编译 Flutter 应用程序的重要工具。它可以将您的 Flutter 项目编译成可以在不同平台上运行的二进制文件或包。</p><p>在 Flutter 中,当你使用 <code>flutter build</code> 命令构建项目时,每个平台的构建产物(build artifacts)将会被存放在项目目录下特定的位置。</p><ol><li>基本语法: flutter build <平台></li><li>支持的平台:<ul><li>apk: 构建 Android APK 文件</li><li>appbundle: 构建 Android App Bundle</li><li>ios: 构建 iOS 应用</li><li>web: 构建 Web 应用</li><li>windows: 构建 Windows 应用</li><li>macos: 构建 macOS 应用</li><li>linux: 构建 Linux 应用</li></ul></li><li>常用选项:<ul><li>–release: 构建发布版本(<strong>默认</strong>)</li><li>–debug: 构建调试版本</li><li>–profile: 构建性能分析版本</li><li>–split-debug-info: 分离调试信息</li><li>–obfuscate: 混淆代码</li><li>–target-platform: 指定目标平台架构</li></ul></li><li>示例:<ul><li>构建 Android APK: flutter build apk</li><li>构建 iOS 应用: flutter build ios</li><li>构建 Web 应用: flutter build web</li></ul></li><li>特殊用法:<ul><li>flutter build aar: 构建 Android Archive (AAR) 文件,用于将 Flutter 模块集成到现有 Android 项目中</li><li>flutter build bundle: 构建 Flutter 资源包,通常用于自定义构建过程</li></ul></li><li>构建过程: build 命令会执行以下步骤:<ul><li>解析项目依赖</li><li>编译 Dart 代码</li><li>生成平台特定的代码</li><li>打包资源文件</li><li>生成最终的二进制文件或包</li></ul></li><li>注意事项:<ul><li>确保在项目根目录下运行命令</li><li>不同平台可能需要额外的设置或工具(如 iOS 需要 Xcode)</li><li>构建时间可能较长,特别是首次构建</li></ul></li><li>优化技巧:<ul><li>使用 –split-debug-info 和 –obfuscate 选项可以减小应用大小并提高安全性</li><li>定期清理构建缓存(flutter clean)可以解决一些构建问题</li></ul></li></ol><h2 id="Android"><a href="#Android" class="headerlink" title="Android"></a><strong>Android</strong></h2><p><strong>基本命令</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter build apk</span><br></pre></td></tr></table></figure><p>对于 Android,构建产物默认位于:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><<span class="keyword">project</span> directory><span class="regexp">/build/</span>app<span class="regexp">/outputs/</span>flutter-apk/</span><br></pre></td></tr></table></figure><p>在这个目录下,你可以找到 APK 文件(如 <code>app-release.apk</code>),这是发布到 Android 设备的文件。</p><p><strong>构建不同架构的 APK</strong></p><p>默认情况下,<code>flutter build apk</code> 会生成一个包含 <code>armeabi-v7a</code> 和 <code>arm64-v8a</code>(32位和64位 ARM 架构)的 APK。如果你需要为其他架构(如 x86)构建 APK 或者想要减小 APK 文件大小,你可以使用 <code>--split-per-abi</code> 选项:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter build apk --split-per-abi</span><br></pre></td></tr></table></figure><p>这将生成针对每个支持的 ABI 的单独 APK 文件,通常用于减少每个 APK 的大小,使其更适合特定设备类型。</p><p>构建完成后,APK 文件默认位于以下路径:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[<span class="keyword">project</span> root]<span class="regexp">/build/</span>app<span class="regexp">/outputs/</span>flutter-apk/</span><br></pre></td></tr></table></figure><p>在这个目录下,你会找到生成的 APK 文件,如:</p><ul><li><code>app-release.apk</code>:未分割 ABI 的发布版本 APK。</li><li><code>app-armeabi-v7a-release.apk</code>:仅包含 <code>armeabi-v7a</code> 架构的发布版本 APK。</li><li><code>app-arm64-v8a-release.apk</code>:仅包含 <code>arm64-v8a</code> 架构的发布版本 APK。</li></ul><p>这些文件可以直接用于安装或分发。如果你使用的是 <code>--debug</code> 或 <code>--profile</code> 选项,相应的文件名也会反映出来(例如 <code>app-debug.apk</code>)。</p><p><strong>Android App Bundle</strong><br>在 Flutter 中,构建一个 Android App Bundle (.aab 文件) 是一个常见的需求,特别是当你需要通过 Google Play 发布你的应用时。Android App Bundle 是 Google 推荐的发布格式,因为它允许 Google Play 根据用户的设备配置动态地生成并提供优化的 APK 包,从而减小应用的下载和安装大小。</p><p>要构建 Android App Bundle,你可以使用以下 Flutter 命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter build appbundle</span><br></pre></td></tr></table></figure><p>构建完成后,App Bundle 文件默认位于以下路径:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[<span class="keyword">project</span> root]<span class="regexp">/build/</span>app<span class="regexp">/outputs/</span>bundle<span class="regexp">/release/</span></span><br></pre></td></tr></table></figure><p>在这个目录下,你会找到生成的 <code>.aab</code> 文件,通常命名为 <code>app-release.aab</code>。</p><p><strong>发布到 Google Play</strong></p><p>生成的 <code>.aab</code> 文件是准备好上传到 Google Play 的。上传 App Bundle 而不是 APK 有几个好处:</p><ol><li><strong>大小优化</strong>:Google Play 会根据每个用户的设备生成并提供最适合的 APK,这通常会减少应用的下载和安装大小。</li><li><strong>简化的版本管理</strong>:你只需上传一个 <code>.aab</code> 文件,Google Play 会处理不同设备所需的各种 APK 分发。</li><li><strong>按需加载</strong>:你可以配置你的应用以按需加载资源和功能,进一步减少初始下载的大小。</li></ol><p><strong>注意事项</strong></p><p>确保在构建 App Bundle 之前,你的应用满足所有必要的 Google Play 要求,包括但不限于适当的 <code>AndroidManifest.xml</code> 配置、API 级别要求和权限声明。此外,如果你的应用使用了特定的硬件或软件功能,确保这些功能在 <code>AndroidManifest.xml</code> 中正确声明。</p><h2 id="iOS"><a href="#iOS" class="headerlink" title="iOS"></a><strong>iOS</strong></h2><p><strong>基本命令</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter build ios</span><br></pre></td></tr></table></figure><p>对于 iOS,构建产物位于:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><<span class="keyword">project</span> directory><span class="regexp">/build/i</span>os<span class="regexp">/iphoneos/</span></span><br></pre></td></tr></table></figure><p>在这个目录中,你会找到一个 <code>.app</code> 目录,这是实际的应用程序包,通常用于模拟器或真实设备的测试。如果你使用 Xcode 进行归档,归档产物将会在 Xcode 的 Organizer 中。</p><h2 id="macOS"><a href="#macOS" class="headerlink" title="macOS"></a><strong>macOS</strong></h2><p><strong>基本命令</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter build macos</span><br></pre></td></tr></table></figure><p>对于 macOS,构建产物位于:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><<span class="keyword">project</span> directory><span class="regexp">/build/m</span>acos<span class="regexp">/Build/</span>Products<span class="regexp">/Release/</span></span><br></pre></td></tr></table></figure><p>这里你可以找到 <code>.app</code> 应用程序包,这是可以直接运行的 macOS 应用。</p><h2 id="Windows"><a href="#Windows" class="headerlink" title="Windows"></a><strong>Windows</strong></h2><p><strong>基本命令</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter build windows</span><br></pre></td></tr></table></figure><p>对于 Windows,构建产物位于:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><<span class="keyword">project</span> directory><span class="regexp">/build/</span>windows<span class="regexp">/x64/</span>runner<span class="regexp">/Release/</span></span><br></pre></td></tr></table></figure><p>或者是 <code>Debug</code> 文件夹,取决于你构建的是发布版还是调试版。在这个目录下,你会找到 <code>.exe</code> 文件及其相关的依赖文件。</p><h2 id="Linux"><a href="#Linux" class="headerlink" title="Linux"></a><strong>Linux</strong></h2><p><strong>基本命令</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter build linux</span><br></pre></td></tr></table></figure><p>对于 Linux,构建产物位于:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><<span class="keyword">project</span> directory><span class="regexp">/build/</span>linux<span class="regexp">/x64/</span>release<span class="regexp">/bundle/</span></span><br></pre></td></tr></table></figure><p>在这个路径中:</p><ul><li><strong>[project root]</strong> 是你的 Flutter 项目的根目录。</li><li><strong>x64</strong> 表示目标架构,目前 Flutter 支持的是 x64(64位)。</li><li><strong>release</strong> 是构建类型,这里是发布版本。如果你构建的是调试版本,这里会是 <code>debug</code>。</li><li><strong>bundle</strong> 目录包含了可执行文件和所有必需的依赖文件,这是最终的发布包。</li></ul><p>在 <code>bundle</code> 目录内,你会找到一个可执行文件(通常以你的项目名命名),以及可能的一些资源文件和库文件。这个可执行文件就是你可以直接运行的 Linux 应用程序。</p><h2 id="Web"><a href="#Web" class="headerlink" title="Web"></a><strong>Web</strong></h2><p><strong>基本命令</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">flutter build web</span><br></pre></td></tr></table></figure><p>对于 Web 平台,构建产物位于:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><<span class="keyword">project</span> directory><span class="regexp">/build/</span>web/</span><br></pre></td></tr></table></figure><p>这个目录包含了所有部署到 Web 服务器的静态文件,如 HTML、JavaScript 和 CSS 文件。</p><p>确保在构建前选择正确的构建配置(如 Debug 或 Release),因为这将影响构建产物的性能和大小。</p><p>在这个 <code>web</code> 目录中,你会找到以下类型的文件和目录:</p><ul><li>**<code>index.html</code>**:这是你的应用的入口文件。如果你使用了 <code>--base-href</code> 选项,<code><base></code> 标签的 <code>href</code> 属性将被设置到你指定的路径。</li><li>**<code>main.dart.js</code>**:这是你的 Dart 代码编译成 JavaScript 后的文件,是运行你的 Flutter 应用的核心。</li><li>**<code>assets/</code>**:包含所有静态资源,如图片、字体以及其他从 Dart 代码中引用的文件。</li><li>**<code>favicon.png</code>**:网站的图标文件。</li><li><strong>其他 JavaScript 和源映射文件</strong>:例如,用于调试的 <code>.map</code> 文件等。</li></ul><p><strong>部署</strong></p><p>当你准备将你的 Flutter Web 应用部署到服务器时,你需要将整个 <code>build/web</code> 目录的内容上传到你的服务器。确保服务器的配置能正确处理你的应用,特别是如果你设置了基本路径(如使用了 <code>--base-href</code>)。</p><p><strong>本地测试</strong></p><p>在部署之前,你可以在本地设置一个 HTTP 服务器来测试这些文件,确保一切按预期工作。你可以使用 Python 的 <code>http.server</code> 模块,或者 Node.js 的 <code>http-server</code>,或者任何其他能够提供静态文件服务的工具。</p><p>例如,使用 Python 的简单 HTTP 服务器:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> [project root]/build/web</span><br><span class="line">python -m http.server 8000</span><br></pre></td></tr></table></figure><p>然后在浏览器中访问 <code>http://localhost:8000</code> 来查看你的应用。</p><p>确保在实际的生产环境中使用专业的 Web 服务器,如 Nginx 或 Apache,以提供更好的性能和安全性。</p>]]></content>
<summary type="html"><p>在 Flutter 中创建一个新项目,同时支持 Android、iOS、macOS、Windows 和 Linux 平台,假设你已经安装了 Flutter SDK 和必要的开发工具(如 Android Studio、Xcode 等)。</p>
<h1 id="安装-Flut</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Flutter" scheme="https://xmaihh.github.io/blog/tags/Flutter/"/>
</entry>
<entry>
<title>Flutter设置App版本</title>
<link href="https://xmaihh.github.io/blog/2024/07/24/flutter-she-zhi-app-ban-ben/"/>
<id>https://xmaihh.github.io/blog/2024/07/24/flutter-she-zhi-app-ban-ben/</id>
<published>2024-07-24T08:59:48.000Z</published>
<updated>2024-07-24T08:59:48.000Z</updated>
<content type="html"><![CDATA[<p>在使用Flutter管理APP版本时,打开<code>pubspec.yaml</code>只看到一个<code>version</code>字段,例如:<code>version: 1.0.0+1</code>。</p><p>我们在使用原生<strong>iOS</strong>或者<strong>Android</strong>开发的时候,我们会在<strong>info.plist</strong>中设置<code>version</code>和<code>build</code>或是在<strong>build.gradle</strong>中设置<code>versionName</code>和<code>versionCode</code>,他们分别表示APP的版本和构建版本。</p><p>拿<strong>Android</strong>开发中简单的说,<code>versionCode</code>是给机器看的,<code>versionName</code>是给人看的。更新的时候,机器根据<code>versionCode</code>判断是升级还是降级,即使<code>versionName(版本号)</code>比以前的高,但是<code>versionCode</code>比以前的低,机器还是会判断是降级。</p><p>Flutter采用的是加号式的版本描述方式,<code>+</code>前面是版本号,<code>+</code>后面是当前版本的build号。格式是 <code>version: major.minor.patch+build</code>,其中 <code>major</code>、<code>minor</code> 和 <code>patch</code> 表示不同的发布级别,<code>build</code> 是构建号。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">version:</span> <span class="number">1.0</span><span class="number">.0</span><span class="string">+1</span></span><br></pre></td></tr></table></figure><p>这里 <code>1.0.0</code> 是版本号,<code>+1</code> 是构建号。每次发布新版本到应用程序商店时,您都应该至少增加构建号。</p><p>在 <strong>Android</strong> 的 <code>android/app/build.gradle</code> 文件中,<code>versionCode</code> 和 <code>versionName</code> 从 <code>pubspec.yaml</code> 文件中获取:</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">android {</span><br><span class="line"> ...</span><br><span class="line"> defaultConfig {</span><br><span class="line"> ...</span><br><span class="line"> versionCode flutterVersionCode.<span class="keyword">toInteger</span>()</span><br><span class="line"> versionName flutterVersionName</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 <strong>iOS</strong> 中,Flutter也会自动更新项目的 <code>Info.plist</code> 文件,但如果您需要手动更新,您可以编辑 <code>CFBundleShortVersionString</code>(版本号)和 <code>CFBundleVersion</code>(构建号):</p><figure class="highlight plist"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- version --></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>CFBundleShortVersionString<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">string</span>></span>$(FLUTTER_BUILD_NAME)<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>CFBundleSignature<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"></span><br><span class="line"><span class="comment"><!-- build --></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>CFBundleVersion<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"><span class="tag"><<span class="name">string</span>></span>$(FLUTTER_BUILD_NUMBER)<span class="tag"></<span class="name">string</span>></span></span><br><span class="line"><span class="tag"><<span class="name">key</span>></span>LSApplicationCategoryType<span class="tag"></<span class="name">key</span>></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><p>Flutter在编译的时候会生成<code>ios/Flutter/Generated.xcconfig</code>和<code>android/local.properties</code>文件。这两个文件由Flutter编译自动生成,不可更改。记录了包含SDK路径或者文件路径,版本信息,环境配置(release/debug)等信息。原生工程获取版本信息的变量就定义在这两个文件里面。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://juejin.cn/post/6844903965440606222">Flutter设置APP版本与构建版本</a></p>]]></content>
<summary type="html"><p>在使用Flutter管理APP版本时,打开<code>pubspec.yaml</code>只看到一个<code>version</code>字段,例如:<code>version: 1.0.0+1</code>。</p>
<p>我们在使用原生<strong>iOS</st</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Flutter" scheme="https://xmaihh.github.io/blog/tags/Flutter/"/>
</entry>
<entry>
<title>Windows下仅为 GitHub 设置SSH代理</title>
<link href="https://xmaihh.github.io/blog/2024/07/15/windows-xia-jin-wei-github-she-zhi-ssh-dai-li/"/>
<id>https://xmaihh.github.io/blog/2024/07/15/windows-xia-jin-wei-github-she-zhi-ssh-dai-li/</id>
<published>2024-07-15T08:06:29.000Z</published>
<updated>2024-07-15T08:06:29.000Z</updated>
<content type="html"><![CDATA[<p> 在测试Windows 10的PowerShell下<code> ssh -T [email protected]</code>命令时,老是报超时。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -t [email protected] ssh: connect to host github.com port 22: connection timed out</span><br></pre></td></tr></table></figure><h1 id="git-代理"><a href="#git-代理" class="headerlink" title="git 代理"></a>git 代理</h1><p>设置 <code>git config --global http.https://github.com.proxy socks5://127.0.0.1:7890</code><br>设置完成后, <code>~/.gitconfig</code> 文件中会增加以下条目:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[<span class="meta">http <span class="string">"https://github.com"</span></span>]</span><br><span class="line"> proxy = socks5:<span class="comment">//127.0.0.1:7890</span></span><br></pre></td></tr></table></figure><h1 id="ssh-代理"><a href="#ssh-代理" class="headerlink" title="ssh 代理"></a>ssh 代理</h1><p>修改 <code>~/.ssh/config</code> 文件:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">Host github.com</span><br><span class="line"> User git</span><br><span class="line"> Port 443</span><br><span class="line"> Hostname ssh.github.com</span><br><span class="line"> IdentityFile <span class="string">"C:\Users\用户名\.ssh\id_rsa"</span></span><br><span class="line"> TCPKeepAlive <span class="built_in">yes</span></span><br><span class="line"> ProxyCommand <span class="string">"C:\Users\用户名\scoop\apps\git\current\mingw64\bin\connect.exe"</span> -S 127.0.0.1:7890 -a none %h %p</span><br><span class="line"></span><br><span class="line">Host ssh.github.com</span><br><span class="line"> User git</span><br><span class="line"> Port 443</span><br><span class="line"> Hostname ssh.github.com</span><br><span class="line"> IdentityFile <span class="string">"C:\Users\用户名\.ssh\id_rsa"</span></span><br><span class="line"> TCPKeepAlive <span class="built_in">yes</span></span><br><span class="line"> ProxyCommand <span class="string">"C:\Users\用户名\scoop\apps\git\current\mingw64\bin\connect.exe"</span> -S 127.0.0.1:7890 -a none %h %p</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><p> 在测试Windows 10的PowerShell下<code> ssh -T [email protected]</code>命令时,老是报超时。</p>
<figure class="highlight bash"><table><tr><td class="gutter"></summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="SSH" scheme="https://xmaihh.github.io/blog/tags/SSH/"/>
<category term="Git" scheme="https://xmaihh.github.io/blog/tags/Git/"/>
<category term="Github" scheme="https://xmaihh.github.io/blog/tags/Github/"/>
<category term="Windows" scheme="https://xmaihh.github.io/blog/tags/Windows/"/>
<category term="Proxy" scheme="https://xmaihh.github.io/blog/tags/Proxy/"/>
</entry>
<entry>
<title>GitHub Actions 自动化部署 Hexo 到Github pages</title>
<link href="https://xmaihh.github.io/blog/2024/07/10/github-actions-zi-dong-hua-bu-shu-hexo-dao-github-pages/"/>
<id>https://xmaihh.github.io/blog/2024/07/10/github-actions-zi-dong-hua-bu-shu-hexo-dao-github-pages/</id>
<published>2024-07-10T14:16:35.000Z</published>
<updated>2024-07-10T14:16:35.000Z</updated>
<content type="html"><![CDATA[<p>好久没写Blog了,准确来说,好久没发布Blog了。由于电脑环境的变化,之前的的环境都找不到了,懒得修,这次切换到自动化部署,以后专心写markdown。</p><h1 id="环境"><a href="#环境" class="headerlink" title="环境"></a>环境</h1><p>在 GitHub 建好两个仓库,</p><ul><li><p>私有仓库,存放<strong>Blog源码仓库</strong></p></li><li><p>公开仓库:存放<strong>Github Pages仓库</strong>:<strong>username.github.io 仓库</strong></p></li></ul><p>Blog源码仓库私有化是一些主题配置有一些API_Token,所以和 Github Pages 仓库分开管理。</p><p>一个仓库也是可以的,直接参考<a href="https://hexo.io/zh-cn/docs/github-pages">Hexo 官方部署方案</a>。</p><h1 id="设置密钥"><a href="#设置密钥" class="headerlink" title="设置密钥"></a>设置密钥</h1><p> GitHub Actions是在<strong>Blog源码仓库</strong>执行的,为了确保 GitHub Actions 能够推送代码到<strong>Github Pages仓库</strong>,使用SSH 密钥的方式来执行git的推送。</p><h2 id="生成新-SSH-密钥"><a href="#生成新-SSH-密钥" class="headerlink" title="生成新 SSH 密钥"></a><a href="https://docs.github.com/zh/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent#generating-a-new-ssh-key">生成新 SSH 密钥</a></h2><p>在本地计算机上生成新的 SSH 密钥。 生成密钥后,可将公钥添加到 GitHub.com 上的帐户中,以便通过 SSH 为 Git 操作启用身份验证。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh-keygen -t ed25519 -C "[email protected]" -f github-deploy-key</span><br></pre></td></tr></table></figure><p>一路回车下去,当前目录下就会生成 <code>github-deploy-key</code> 和 <code>github-deploy-key.pub</code> 。</p><blockquote><p>千万注意:在提示符下,键入安全密码的时候,直接回车不要输入密码。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">> Enter passphrase (empty <span class="keyword">for</span> no passphrase): [Type a passphrase]</span><br><span class="line">> Enter same passphrase again: [Type passphrase again] </span><br></pre></td></tr></table></figure><p>如果输入了密码,那么重新生成一个吧。</p></blockquote><h2 id="向你的Github帐户添加新的-SSH-密钥"><a href="#向你的Github帐户添加新的-SSH-密钥" class="headerlink" title="向你的Github帐户添加新的 SSH 密钥"></a><del><a href="https://docs.github.com/zh/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account#adding-a-new-ssh-key-to-your-account">向你的Github帐户添加新的 SSH 密钥</a></del></h2><p><del>向 GitHub.com 上的帐户添加SSH 公钥<code>github-deploy-key.pub</code>的内容。</del></p><ol><li><p><del>在 GitHub 任意页的右上角,单击个人资料照片,然后单击“<strong>设置</strong>”。</del></p></li><li><p><del>在边栏的“访问”部分中,单击 “SSH 和 GPG 密钥”。</del></p></li><li><p><del>单击“新建 SSH 密钥”或“添加 SSH 密钥” 。</del></p></li><li><p><del>在 “Title”(标题)字段中,为新密钥添加描述性标签。</del></p></li><li><p><del>在“密钥”字段中,粘贴公钥。</del></p></li><li><p><del>单击“添加 SSH 密钥”。</del></p><p><img src="https://s2.loli.net/2024/07/11/SwXtxivCycJZ4f7.png" alt="image-20240711173259866"></p></li></ol><blockquote><p><del>为了保险起见,你可以在本地先测试下 SSH 连接,确保设置成功。</del></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh -T [email protected]</span><br></pre></td></tr></table></figure></blockquote><h1 id="Blog源码仓库设置"><a href="#Blog源码仓库设置" class="headerlink" title="Blog源码仓库设置"></a><strong>Blog源码仓库</strong>设置</h1><p>借助存储库环境中创建的环境变量Secrets and variables设置SSH 私钥,GitHub Actions 读取并保存下来还原成SSH 私钥文件。</p><p>进入<strong>Blog源码仓库</strong>页面 → Settings → Secrets and variables → actions → New repository secret,Name 填 <code>HEXO_DEPLOY_PRI</code> ,Secret 填 <code>github-deploy-key</code> 私钥的内容。</p><p><img src="https://s2.loli.net/2024/07/11/VZ2Kzjw1C7dEXxi.png" alt="image-20240711175319943"></p><h1 id="Github-Pages仓库设置"><a href="#Github-Pages仓库设置" class="headerlink" title="Github Pages仓库设置"></a><strong>Github Pages仓库</strong>设置</h1><p>进入<strong>Github Pages仓库</strong>页面 → Settings → Deploy keys → Add deploy key,Title 填 <code>HEXO_DEPLOY_PUB</code> ,Key 填 <code>github-deploy-key.pub</code> 公钥的内容。</p><p><img src="https://s2.loli.net/2024/07/12/YWUOpze79IPuQLJ.png" alt="image-YWUOpze79IPuQLJ"></p><blockquote><p>这里有一个坑,如果你在[2.2 向你的Github账户添加新的SSH密钥](## <del><a href="https://docs.github.com/zh/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account#adding-a-new-ssh-key-to-your-account">向你的Github帐户添加新的 SSH 密钥</a></del>)会发现这里添加不上,来看一下Github对<a href="https://docs.github.com/en/authentication/connecting-to-github-with-ssh/managing-deploy-keys#deploy-keys">Deploy keys 部署密钥</a>的说明:部署密钥是授予对<strong>单个存储库的访问权限</strong>的 SSH 密钥。GitHub 将<strong>密钥的公共部分</strong>直接附加到您的存储库(这里是Github Pages仓库)而不是个人帐户,并且<strong>密钥的私有部分</strong>保留在您的服务器上(这里是执行Github Action的Blog源码仓库)。</p></blockquote><h1 id="升级最新版本的Hexo"><a href="#升级最新版本的Hexo" class="headerlink" title="升级最新版本的Hexo"></a>升级最新版本的Hexo</h1><p>我之前的Hexo版本是5.0+的版本了,怕在部署的时候遇到一些版本问题,所以直接重新安装最新版本的Hexo,在本地调试好确保可以在本地正常生成和部署页面。</p><p>参考Hexo官方文档:<a href="https://hexo.io/docs/">https://hexo.io/docs/</a></p><p>直接安装好环境,把<strong>旧的source 文件夹</strong>迁移过去即可。</p><blockquote><p>注意更新<code>package.json</code>中的所有依赖包到最新版本,使用 <code>npm-check-updates</code> 是一个有用的工具,可以用来更新 <code>package.json</code> 中的所有依赖包到最新的版本。<strong>全局安装 npm-check-updates</strong>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install -g npm-check-updates</span><br></pre></td></tr></table></figure><p><strong>检查可更新的依赖</strong>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ncu</span><br></pre></td></tr></table></figure><p>这会列出所有可以更新的依赖包及其新版本。</p><p><strong>更新 <code>package.json</code> 中的依赖版本</strong>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ncu -u</span><br></pre></td></tr></table></figure><p>这会自动更新 <code>package.json</code> 中的依赖版本。</p><p><strong>安装更新的依赖</strong>:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">npm install</span><br></pre></td></tr></table></figure><p>这会列出所有可以更新的依赖包及其新版本。</p><p><strong>更新特定依赖</strong></p><p>如果你只想更新某个特定的依赖包,可以使用 <code>npm install</code> 命令并指定最新版本或使用 <code>@latest</code> 标签。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">npm install [package-name]@latest</span><br><span class="line">例如,要更新 hexo 到最新版本</span><br><span class="line">npm install hexo@latest</span><br></pre></td></tr></table></figure></blockquote><h1 id="Github-Actions-脚本"><a href="#Github-Actions-脚本" class="headerlink" title="Github Actions 脚本"></a>Github Actions 脚本</h1><p>在你的<strong>Blog源码仓库</strong>根目录下创建一个 <strong>.github/workflows</strong> 文件夹,在该文件夹内创建一个新的 <strong>YAML</strong> 文件(例如 hexo-deploy.yml)用于定义 <strong>GitHub Actions</strong> 工作流。</p><p>完整的 GitHub Actions 配置文件内容如下:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br></pre></td><td class="code"><pre><span class="line">name: Deploy hexo blog</span><br><span class="line"></span><br><span class="line"><span class="comment"># 设置触发条件</span></span><br><span class="line"><span class="comment"># 触发条件:push更新到master 分支 触发 GitHub Actions 进行自动部署</span></span><br><span class="line">on:</span><br><span class="line"> push:</span><br><span class="line"> branches:</span><br><span class="line"> - master <span class="comment"># Blog源码仓库你使用的分支名称</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">jobs</span>:</span><br><span class="line"> build:</span><br><span class="line"> <span class="comment"># 使用ubuntu镜像,node版本为20.x</span></span><br><span class="line"> runs-on: ubuntu-latest</span><br><span class="line"> strategy:</span><br><span class="line"> matrix:</span><br><span class="line"> node-version: [20.x]</span><br><span class="line"></span><br><span class="line"> steps:</span><br><span class="line"> <span class="comment"># 拉取代码</span></span><br><span class="line"> - name: Checkout</span><br><span class="line"> uses: actions/checkout@v4</span><br><span class="line"></span><br><span class="line"> <span class="comment"># 安装node和npm</span></span><br><span class="line"> - name: Use Node.js <span class="variable">${{ matrix.node-version }</span>}</span><br><span class="line"> uses: actions/setup-node@v4</span><br><span class="line"> with:</span><br><span class="line"> node-version: <span class="variable">${{ matrix.node-version }</span>}</span><br><span class="line"> </span><br><span class="line"> <span class="comment"># 配置环境变量,也可使用第三方的 action 发布到gh-pages,如peaceiris/actions-gh-pages@v3,就不用配置这么复杂</span></span><br><span class="line"> - name: Configuration environment</span><br><span class="line"> <span class="built_in">env</span>:</span><br><span class="line"> HEXO_DEPLOY_PRI: <span class="variable">${{secrets.HEXO_DEPLOY_PRI}</span>}</span><br><span class="line"> run: |</span><br><span class="line"> <span class="built_in">sudo</span> timedatectl set-timezone <span class="string">"Asia/Shanghai"</span></span><br><span class="line"> <span class="built_in">mkdir</span> -p ~/.ssh/</span><br><span class="line"> <span class="built_in">echo</span> <span class="string">"<span class="variable">$HEXO_DEPLOY_PRI</span>"</span> | <span class="built_in">tr</span> -d <span class="string">'\r'</span> > ~/.ssh/id_rsa</span><br><span class="line"> <span class="built_in">chmod</span> 600 ~/.ssh/id_rsa</span><br><span class="line"> ssh-keyscan github.com >> ~/.ssh/known_hosts</span><br><span class="line"> git config --global user.name <span class="string">"yourname"</span></span><br><span class="line"> git config --global user.email <span class="string">"your_email"</span></span><br><span class="line"></span><br><span class="line"> <span class="comment"># npm ci 使用 package-lock.json 文件中的确切版本来安装依赖</span></span><br><span class="line"> <span class="comment"># 我这里用的主题为maupassant</span></span><br><span class="line"> - name: Install dependencies</span><br><span class="line"> run: |</span><br><span class="line"> npm i -g hexo-cli</span><br><span class="line"> npm ci</span><br><span class="line"> <span class="built_in">cd</span> themes/maupassant/</span><br><span class="line"> npm ci</span><br><span class="line"></span><br><span class="line"> - name: Deploy hexo</span><br><span class="line"> run: |</span><br><span class="line"> hexo clean</span><br><span class="line"> hexo d -g</span><br></pre></td></tr></table></figure><blockquote><p>配置部署信息</p><p>要使 <code>hexo d -g</code> 正常工作,你需要在Blog源码根目录下的 <code>_config.yml</code> 文件中配置部署信息。配置如下:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">deploy:</span><br><span class="line"> <span class="built_in">type</span>: git</span><br><span class="line"> repo: [email protected]:username/username.github.io <span class="comment"># 更改为你的 GitHub Pages 仓库, username 是你的用户名</span></span><br><span class="line"> branch: gh-pages <span class="comment"># GitHub Pages 分支</span></span><br></pre></td></tr></table></figure></blockquote><p>提交本地的 <strong>Blog源码仓库</strong>即可触发 <strong>Github Actions</strong> 工作流实现自动部署,然后访问你的 <strong>username.github.io</strong> !</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://alanlee.fun/2024/07/05/deploy-hexo-with-github-action/">使用 GitHub Actions 自动发布 Hexo 博客</a><br><a href="https://hackergavin.com/2024/01/11/hexo-automate-deploy/">利用 GitHub Actions 实现自动化部署 Hexo 到 Github Pages</a></p>]]></content>
<summary type="html"><p>好久没写Blog了,准确来说,好久没发布Blog了。由于电脑环境的变化,之前的的环境都找不到了,懒得修,这次切换到自动化部署,以后专心写markdown。</p>
<h1 id="环境"><a href="#环境" class="headerlink" title="环境"</summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Github Actions" scheme="https://xmaihh.github.io/blog/tags/Github-Actions/"/>
<category term="Hexo" scheme="https://xmaihh.github.io/blog/tags/Hexo/"/>
<category term="CI/CD" scheme="https://xmaihh.github.io/blog/tags/CI-CD/"/>
</entry>
<entry>
<title>Cloudflare Tunnel(实现内网穿透)的使用方法</title>
<link href="https://xmaihh.github.io/blog/2024/07/10/cloudflare-tunnel-shi-xian-nei-wang-chuan-tou-de-shi-yong-fang-fa/"/>
<id>https://xmaihh.github.io/blog/2024/07/10/cloudflare-tunnel-shi-xian-nei-wang-chuan-tou-de-shi-yong-fang-fa/</id>
<published>2024-07-10T10:55:12.000Z</published>
<updated>2024-07-10T10:55:12.000Z</updated>
<content type="html"><![CDATA[<p>Cloudflare Tunnel提供了一种安全的方法来连接你的网络服务到 Cloudflare 网络,而不需要开放服务器的端口到公网上,或者在 DNS 上直接暴露服务器的 IP 地址。这种方式能够帮助越过 DNS 阻断,并增强服务的安全性。</p><p>由于服务的真实 IP 地址不会在 DNS 查询中直接暴露,Cloudflare Tunnel 可以帮助绕过基于 DNS 的阻断。用户的请求首先到达 Cloudflare 的网络,然后通过建立好的安全隧道转发到后端的服务。这意味着,即使某些 DNS 请求被拦截或阻断,用户的请求仍然可以通过 Cloudflare 的网络到达目标服务。</p><p><a href="https://www.cloudflare.com/products/tunnel/">Cloudflare Tunnel</a> 可以让服务器主动访问 Cloudflare (CF) 的 CDN 节点。这样服务器完全不需要接受任何外来连接,增强安全性。此外,Cloudflare Tunnel 也可以让服务器处于 NAT 后面,无需公网 IP 也可以提供 Web 等服务。</p><blockquote><p>要使用 Cloudflare Tunnel,相应域名必须在 CF 解析,且服务器必须能够访问最近的 CF 节点。</p></blockquote><p>本文以搭建 <code>memos</code> 为例子,类似于一个私人微博的开源项目</p><p><a href="https://github.com/usememos/memos">https://github.com/usememos/memos</a></p><p><img src="https://s2.loli.net/2024/07/10/lykGfBTxHsdtwFP.png" alt="demo-screenshot"></p><p>域名以二级域名:<code>memos.abc.com</code> 为例。</p><h1 id="memos安装"><a href="#memos安装" class="headerlink" title="memos安装"></a><code>memos</code>安装</h1><p>直接docker compose起手:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">mkdir</span> ~/memos && <span class="built_in">cd</span> ~/memos</span><br><span class="line">$ <span class="built_in">echo</span> <span class="string">"services:</span></span><br><span class="line"><span class="string"> memos:</span></span><br><span class="line"><span class="string"> image: neosmemo/memos:stable</span></span><br><span class="line"><span class="string"> container_name: memos</span></span><br><span class="line"><span class="string"> volumes:</span></span><br><span class="line"><span class="string"> - ~/.memos/:/var/opt/memos</span></span><br><span class="line"><span class="string"> ports:</span></span><br><span class="line"><span class="string"> - \"5230:5230\""</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> docker-compose.yml > /dev/null</span><br><span class="line">$ docker compose up -d</span><br></pre></td></tr></table></figure><p>访问<code>http://localhost:5230</code>,即可打开<code>memos</code>。</p><h1 id="Nginx设置反向代理(非必要步骤)"><a href="#Nginx设置反向代理(非必要步骤)" class="headerlink" title="Nginx设置反向代理(非必要步骤)"></a>Nginx设置反向代理(非必要步骤)</h1><p>安装Nginx:</p><p>添加<code>memos</code>的反向代理配置:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">echo</span> <span class="string">"server {</span></span><br><span class="line"><span class="string"> listen 80;</span></span><br><span class="line"><span class="string"> listen [::]:80;</span></span><br><span class="line"><span class="string"> server_name memos.abc.com;</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string"> location / {</span></span><br><span class="line"><span class="string"> proxy_pass http://localhost:5230;</span></span><br><span class="line"><span class="string"> proxy_set_header Host \$host;</span></span><br><span class="line"><span class="string"> proxy_set_header X-Real-IP \$remote_addr;</span></span><br><span class="line"><span class="string"> proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;</span></span><br><span class="line"><span class="string"> proxy_set_header X-Forwarded-Proto \$scheme;</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string">}"</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> /etc/nginx/conf.d/memos.abc.com.conf > /dev/null</span><br></pre></td></tr></table></figure><p>重启Nginx,使配置生效:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> systemctl restart nginx</span><br></pre></td></tr></table></figure><p>报错:<code>[Nginx: Failed to start A high performance web server and a reverse proxy server](https://stackoverflow.com/questions/51525710/nginx-failed-to-start-a-high-performance-web-server-and-a-reverse-proxy-server)</code></p><blockquote><p>您已经有一个进程绑定到 HTTP 端口 80。您可以运行命令 <code>sudo lsof -i:80</code> 来获取使用该端口的进程列表,然后尝试停止正在使用80端口的进程<code>sudo fuser -k 80/tcp</code>。</p></blockquote><p>对我来说,这个错误是由端口 80 上已有的 <code>default</code> nginx 站点引起的。删除默认站点有效。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">sudo</span> <span class="built_in">rm</span> /etc/nginx/sites-enabled/default</span><br><span class="line">$ <span class="built_in">sudo</span> service nginx restart</span><br></pre></td></tr></table></figure><h1 id="安装Cloudflared"><a href="#安装Cloudflared" class="headerlink" title="安装Cloudflared"></a>安装Cloudflared</h1><h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ curl -L <span class="string">'https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64'</span> -o cloudflared && <span class="built_in">chmod</span> +x cloudflared</span><br></pre></td></tr></table></figure><h2 id="登录"><a href="#登录" class="headerlink" title="登录"></a>登录</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ./cloudflared tunnel login</span><br></pre></td></tr></table></figure><p>输入命令后,终端会给出一个登陆地址,我们拷贝到浏览器里面打开,选择需要授权的域名,完成后将生成一个证书文件 <code>~/.cloudflared/cert.pem</code>。</p><h2 id="创建隧道"><a href="#创建隧道" class="headerlink" title="创建隧道"></a>创建隧道</h2><p>授权完以后,我们需要创建隧道。一般建议一台服务器创建一个隧道。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">cloudflared tunnel create <隧道名字></span><br><span class="line"><span class="comment"># 比如</span></span><br><span class="line">$ ./cloudflared tunnel create memos</span><br></pre></td></tr></table></figure><p>创建完以后,会输出隧道的一个UUID,记录下来Tunnel ID。</p><h2 id="创建-DNS-记录"><a href="#创建-DNS-记录" class="headerlink" title="创建 DNS 记录"></a>创建 DNS 记录</h2><p>我们需要把域名指向到对应的隧道。</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">cloudflared tunnel route dns <隧道名字> <域名></span><br><span class="line"><span class="comment"># 比如一级域名(和Web界面不一样,不需要输入@)</span></span><br><span class="line">cloudflared tunnel route dns memos abc.com</span><br><span class="line"><span class="comment"># 又比如二级域名</span></span><br><span class="line">cloudflared tunnel route dns memos memos.abc.com</span><br><span class="line">$ ./cloudflared tunnel route dns memos memos.abc.com</span><br></pre></td></tr></table></figure><p>这时候,Cloudflare会自动添加一条CNAME记录到对应的域名。</p><p>对于多个其他域名,我们需要登录Cloudflare的Web控制台,对应添加CNAME记录,记录值是:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><隧道UUID>.cfargotunnel.com</span><br><span class="line"></span><br><span class="line">比如</span><br><span class="line"></span><br><span class="line">12345-123-123-123-12345.cfargotunnel.com</span><br></pre></td></tr></table></figure><p><img src="https://s2.loli.net/2024/07/10/fNEl6vMZUpCz4Th.png" alt="img"></p><h2 id="cloudflared-配置"><a href="#cloudflared-配置" class="headerlink" title="cloudflared 配置"></a>cloudflared 配置</h2><p>开始配置Cloudflared,先编辑一个配置文件<code>vim ~/.cloudflared/config.yml</code></p><p>输入下面的内容(根据自己要求编辑)</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">tunnel: <隧道UUID></span><br><span class="line">credentials-file: /root/.cloudflared/<隧道UUID>.json</span><br><span class="line">protocol: h2mux</span><br><span class="line">ingress:</span><br><span class="line"> <span class="comment"># 第一个网站,连接到本地的80端口</span></span><br><span class="line"> - hostname: <域名1.com></span><br><span class="line"> service: http://localhost:80</span><br><span class="line"> <span class="comment"># 第二个网站,https协议,连接到本地的443端口,禁用证书校验(用于自签名SSL证书)</span></span><br><span class="line"> - hostname: <域名2.com></span><br><span class="line"> service: https://127.0.0.1:443</span><br><span class="line"> originRequest:</span><br><span class="line"> noTLSVerify: <span class="literal">true</span></span><br><span class="line"> originServerName: <域名2.com></span><br><span class="line"> <span class="comment"># 第三个网站,8012端口,泛域名</span></span><br><span class="line"> - hostname: <*.域名3.com></span><br><span class="line"> service: http://localhost:8012</span><br><span class="line"> <span class="comment"># 第四个,反代MySQL sock服务</span></span><br><span class="line"> - hostname: <mysql.域名4.com></span><br><span class="line"> service: unix:/tmp/mysql.sock</span><br><span class="line"> <span class="comment"># 第五个,反代SSH服务</span></span><br><span class="line"> - hostname: <ssh.域名5.com></span><br><span class="line"> service: ssh://localhost:22</span><br><span class="line"> - service: http_status:404</span><br></pre></td></tr></table></figure><p>更多支持的服务和配置方式,参考帮助文档:<a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-guide/local/local-management/ingress/?ref=bra.live#supported-protocols">Supported protocols</a></p><p>我这里配置:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="built_in">echo</span> <span class="string">"tunnel: <隧道UUID></span></span><br><span class="line"><span class="string">credentials-file: /root/.cloudflared/<隧道UUID>.json</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"><span class="string">ingress:</span></span><br><span class="line"><span class="string"> - hostname: memos.abc.com</span></span><br><span class="line"><span class="string"> service: http://localhost:80</span></span><br><span class="line"><span class="string"> - service: http_status:404"</span> | <span class="built_in">sudo</span> <span class="built_in">tee</span> ~/.cloudflared/config.yml > /dev/null</span><br></pre></td></tr></table></figure><p>验证下配置有没有问题</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ./cloudflared tunnel ingress validate</span><br></pre></td></tr></table></figure><p>再测试下规则是否命中</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">./cloudflared tunnel ingress rule https://memos.abc.com</span></span><br></pre></td></tr></table></figure><p>如果没问题,OK,一切妥当,我们开始测试</p><figure class="highlight jboss-cli"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ <span class="string">./cloudflared</span> <span class="params">--loglevel</span> debug <span class="params">--transport-loglevel</span> warn <span class="params">--config</span> ~<span class="string">/.cloudflared/config.yml</span> tunnel run <隧道UUID></span><br></pre></td></tr></table></figure><p>终端会输出一大堆log,但没有红色报错,那就没问题。</p><p>登陆Cloudflare Zero Trust的<a href="https://one.dash.cloudflare.com/?ref=bra.live">Web控制台</a>,左边选择Networks-Tunnels,可以看到隧道已经跑起来了,状态是<code>HEALTHY</code>。</p><p><img src="https://s2.loli.net/2024/07/10/dIBYseOKuZ2w5Nc.png" alt="image-20240710163347443"></p><p>按下Ctrl+z,先停掉刚才启动的服务。</p><h2 id="配置为系统服务"><a href="#配置为系统服务" class="headerlink" title="配置为系统服务"></a>配置为系统服务</h2><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">$ ./cloudflared service install</span><br></pre></td></tr></table></figure><blockquote><p>创建系统服务后,配置文件会被拷贝到 <code>/etc/cloudflared/config.yml</code>。</p></blockquote><p>在创建系统服务后,你可以使用 <code>systemctl</code> 命令来管理服务。</p><h4 id="1-启动服务"><a href="#1-启动服务" class="headerlink" title="1. 启动服务"></a>1. 启动服务</h4><p>要启动 <code>cloudflared</code> 服务,使用以下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl start cloudflared</span><br></pre></td></tr></table></figure><h4 id="2-停止服务"><a href="#2-停止服务" class="headerlink" title="2. 停止服务"></a>2. 停止服务</h4><p>要停止 <code>cloudflared</code> 服务,使用以下命令:</p><figure class="highlight arduino"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl stop cloudflared</span><br></pre></td></tr></table></figure><h4 id="3-重启服务"><a href="#3-重启服务" class="headerlink" title="3. 重启服务"></a>3. 重启服务</h4><p>要重启 <code>cloudflared</code> 服务,使用以下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl restart cloudflared</span><br></pre></td></tr></table></figure><h4 id="4-查看服务状态"><a href="#4-查看服务状态" class="headerlink" title="4. 查看服务状态"></a>4. 查看服务状态</h4><p>要查看 <code>cloudflared</code> 服务的状态,使用以下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">bash</span><br><span class="line">复制代码</span><br><span class="line"><span class="built_in">sudo</span> systemctl status cloudflared</span><br></pre></td></tr></table></figure><p>这将显示服务的运行状态、最近的日志等信息。</p><h4 id="5-启用服务开机自启动"><a href="#5-启用服务开机自启动" class="headerlink" title="5. 启用服务开机自启动"></a>5. 启用服务开机自启动</h4><p>要设置 <code>cloudflared</code> 服务在系统启动时自动启动,使用以下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> cloudflared</span><br></pre></td></tr></table></figure><h4 id="6-禁用服务开机自启动"><a href="#6-禁用服务开机自启动" class="headerlink" title="6. 禁用服务开机自启动"></a>6. 禁用服务开机自启动</h4><p>要禁用 <code>cloudflared</code> 服务在系统启动时自动启动,使用以下命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> systemctl <span class="built_in">disable</span> cloudflared</span><br></pre></td></tr></table></figure><h4 id="7-查看服务日志"><a href="#7-查看服务日志" class="headerlink" title="7. 查看服务日志"></a>7. 查看服务日志</h4><p>要查看 <code>cloudflared</code> 服务的日志,使用 <code>journalctl</code> 命令:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> journalctl -u cloudflared</span><br></pre></td></tr></table></figure><h3 id="访问站点"><a href="#访问站点" class="headerlink" title="访问站点"></a>访问站点</h3><p><code>cloudflared</code>启动后</p><p>打开网址 <code>memos.abc.com</code>,可看到我们已能成功访问到搭建的 memos 站点。</p>]]></content>
<summary type="html"><p>Cloudflare Tunnel提供了一种安全的方法来连接你的网络服务到 Cloudflare 网络,而不需要开放服务器的端口到公网上,或者在 DNS 上直接暴露服务器的 IP 地址。这种方式能够帮助越过 DNS 阻断,并增强服务的安全性。</p>
<p>由于服务的真实 </summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Linux" scheme="https://xmaihh.github.io/blog/tags/Linux/"/>
<category term="Cloudflare" scheme="https://xmaihh.github.io/blog/tags/Cloudflare/"/>
<category term="Docker" scheme="https://xmaihh.github.io/blog/tags/Docker/"/>
<category term="docker-compose" scheme="https://xmaihh.github.io/blog/tags/docker-compose/"/>
</entry>
<entry>
<title>Frp内网穿透服务端+客户端配置</title>
<link href="https://xmaihh.github.io/blog/2024/07/09/frp-nei-wang-chuan-tou-fu-wu-duan-ke-hu-duan-pei-zhi/"/>
<id>https://xmaihh.github.io/blog/2024/07/09/frp-nei-wang-chuan-tou-fu-wu-duan-ke-hu-duan-pei-zhi/</id>
<published>2024-07-09T14:55:12.000Z</published>
<updated>2024-07-09T14:55:12.000Z</updated>
<content type="html"><![CDATA[<h1 id="工作环境"><a href="#工作环境" class="headerlink" title="工作环境"></a>工作环境</h1><ul><li>具备公网 IP 的云服务器(Ubuntu22.04)</li><li>黑群晖DS918+</li><li>Google Chrome 126.0.6478.127</li><li>Frp v0.58.1</li><li>SSH工具</li></ul><p>FRP (Fast Reverse Proxy) 是一款高性能的内网穿透工具,<a href="https://github.com/fatedier/frp">https://github.com/fatedier/frp</a></p><p>FRP 分为两部分:frps(FRP 服务器),frpc(FRP 客户端)。</p><p>frps 需要部署在有公网 IP 的服务器上,而 frpc 部署在内网中的机器。</p><h1 id="安装Frps(Frp服务器)"><a href="#安装Frps(Frp服务器)" class="headerlink" title="安装Frps(Frp服务器)"></a>安装Frps(Frp服务器)</h1><p>用SSH工具连上具备公网IP的云服务器,执行一键脚本需要<code>sudo -i</code>切换到root用户</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">wget https://raw.githubusercontent.com/mvscode/frps-onekey/master/install-frps.sh -O ./install-frps.sh</span><br><span class="line">chmod 700 ./install-frps.sh</span><br><span class="line">./install-frps.sh install</span><br></pre></td></tr></table></figure><p>接着脚本会让你依次设置各种端口,为了避免冲突,建议手动设置各个端口号.最终会出现一个汇总信息,如果没问题就继续即可安装完成。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">============== Check your input ==============</span><br><span class="line">You Server IP : 你的公网IP</span><br><span class="line">Bind port : 7000</span><br><span class="line">vhost http port : 8800</span><br><span class="line">vhost https port : 8443</span><br><span class="line">Dashboard port : 8555</span><br><span class="line">Dashboard user : admin</span><br><span class="line">Dashboard password : p@ssw0rd</span><br><span class="line">token : sk-HMZUjKMPHHedvuxrhNmNoRIAxEnEGO</span><br><span class="line">subdomain_host : 没有就填公网IP</span><br><span class="line">tcp mux : true</span><br><span class="line">Max Pool count : 5</span><br><span class="line">Log level : info</span><br><span class="line">Log max days : 3</span><br><span class="line">Log file : frps.log</span><br><span class="line">transport protocol : enable</span><br><span class="line">kcp bind port : 23043</span><br><span class="line">quic bind port : 23045</span><br><span class="line">==============================================</span><br></pre></td></tr></table></figure><p>安装好后会自动运行frps,即可访问 IP + Dashboard port 端口号进入frps管理网页,http://你的公网IP:8555,输入设置的用户名和密码,能成功进入 WEB 后台就成功了。</p><p>常用命令:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">Usage: /etc/init.d/frps {start|stop|restart|status|config|version}</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Uninstall(卸载)</span></span><br><span class="line">./install-frps.sh uninstall</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">Update(更新)</span></span><br><span class="line">./install-frps.sh update</span><br><span class="line"></span><br></pre></td></tr></table></figure><h1 id="安装FRPC(Frp客户端)"><a href="#安装FRPC(Frp客户端)" class="headerlink" title="安装FRPC(Frp客户端)"></a>安装FRPC(Frp客户端)</h1><p><strong>frpc.toml</strong> 是 frp 客户端中重要的配置文件,错误的配置会导致服务无法访问,参考以下文档仔细修改每条参数。</p><p>配置模板:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line">serverAddr = "你的公网IP" # frps 服务器地址</span><br><span class="line">serverPort = 7000 # frps 服务器端口</span><br><span class="line">auth.method = "token"</span><br><span class="line">auth.token = "sk-HMZUjKMPHHedvuxrhNmNoRIAxEnEGO" # frps 服务器token</span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">穿透需要Web访问的内网服务,例如群晖的DSM管理页面</span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">当 <span class="built_in">type</span> = <span class="string">"http"</span> 或者 <span class="string">"https"</span> 协议时,</span> </span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">custom_domains 和 subdomain 至少需要任意一条参数,</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">也可以同时存在。如果没有此参数会导致 frp 客户端无法启动</span></span><br><span class="line"></span><br><span class="line">[[proxies]]</span><br><span class="line">name = "DSM" #该条穿透服务的名称,必须修改,且不能与其他用户重复</span><br><span class="line">type = "http" </span><br><span class="line">localIp = "192.168.50.2" # 本地端口</span><br><span class="line">localPort = 5000 # 本地端口</span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">customDomains = [<span class="string">"192.168.50.2"</span>]</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">subdomain = <span class="string">"nas"</span></span></span><br><span class="line"><span class="meta prompt_"></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">穿透需要TCP连接的内网服务,例如 SSH的22端口或者Windows RDP的3389端口</span></span><br><span class="line">[[proxies]]</span><br><span class="line">name = "SSH"</span><br><span class="line">type = "tcp"</span><br><span class="line">localIp = "192.168.50.2"</span><br><span class="line">localPort = 22</span><br><span class="line">remotePort = 33078 # frps分配的端口,需要在防火墙中放行</span><br><span class="line"></span><br><span class="line">[[proxies]]</span><br><span class="line">name = "RDP"</span><br><span class="line">type = "tcp"</span><br><span class="line">localIp = "192.168.50.2"</span><br><span class="line">localPort = 3389</span><br><span class="line">remotePort = 33079 # frps分配的端口,需要在防火墙中放行</span><br></pre></td></tr></table></figure><blockquote><p><strong>重点提示:</strong>当 <strong>type = tcp</strong> 时,无需配置上文的两条域名记录,可以直接使用 frp 服务器的地址作为域名,也可以将自己的域名 CNAME 或 A 记录 指向 frp 服务器的域名或 IP。</p></blockquote><p><strong>对于 Windows:</strong></p><ol><li><p>从<a href="https://github.com/fatedier/frp">官方 GitHub 仓库</a>下载 FRP 客户端</p></li><li><p>创建一个配置文件(例如 frpc.toml)并设置您的配置</p></li><li><p>使用 NSSM(Non-Sucking Service Manager)创建 Windows 服务: a. 从 <a href="https://nssm.cc/">https://nssm.cc/</a> 下载 NSSM b. 以管理员身份打开命令提示符 c. 导航到 NSSM 目录 d. 运行保存为<code>install.bat</code>并运行:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">: 1. download [nssm](https://github.com/kirillkovalenko/nssm) and set its diretory into the global PATH environment value</span><br><span class="line">: 2. replace the path below with where you placed frpc</span><br><span class="line">nssm install frpc "D:\Tools\frpc\frpc.exe" -c "D:\Tools\frpc\frpc.toml"</span><br><span class="line">nssm set frpc DisplayName "frp client"</span><br><span class="line">nssm start frpc</span><br></pre></td></tr></table></figure></li></ol><blockquote><p>如果需要,使用 <code>nssm.exe edit FRPClient</code> 配置额外的服务参数</p></blockquote><ol start="4"><li><p>卸载服务,将代码保存为<code>uninstall.bat</code>并运行:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">nssm stop frpc</span><br><span class="line">nssm remove frpc</span><br></pre></td></tr></table></figure></li></ol><p><strong>对于 Ubuntu/Debian:</strong></p><ol><li><p>从官方 GitHub 仓库下载 Linux 版的 FRP 客户端。</p></li><li><p>解压文件并将它们移动到合适的位置,例如 /usr/local/frp/</p></li><li><p>创建一个配置文件(例如 /usr/local/frp/frpc.toml)并设置您的配置。</p></li><li><p>创建一个 systemd 服务文件:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> nano /etc/systemd/system/frpc.service</span><br></pre></td></tr></table></figure></li><li><p>在文件中添加以下内容:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"> [Unit]</span><br><span class="line"> Description=FRP Client</span><br><span class="line"> After=network.target</span><br><span class="line"> </span><br><span class="line"> [Service]</span><br><span class="line"> Type=simple</span><br><span class="line"> User=nobody</span><br><span class="line"> Restart=on-failure</span><br><span class="line"> RestartSec=5s</span><br><span class="line"> ExecStart=/usr/local/frp/frpc -c /usr/local/frp/frpc.toml</span><br><span class="line"> </span><br><span class="line"> [Install]</span><br><span class="line"> WantedBy=multi-user.target</span><br><span class="line"></span><br><span class="line">6. 保存文件并退出编辑器。</span><br><span class="line"></span><br><span class="line">7. 重新加载 systemd,启用并启动服务检查状态。</span><br><span class="line"></span><br><span class="line"> ```bash</span><br><span class="line"> <span class="built_in">sudo</span> systemctl daemon-reload</span><br><span class="line"> <span class="built_in">sudo</span> systemctl <span class="built_in">enable</span> frpc</span><br><span class="line"> <span class="built_in">sudo</span> systemctl start frpc</span><br><span class="line"> <span class="built_in">sudo</span> systemctl status frpc</span><br></pre></td></tr></table></figure></li></ol><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://github.com/fatedier/frp/tree/dev">官方文档</a></p><p><a href="https://freefrp.net/docs">详解 frpc.toml 配置文件</a></p>]]></content>
<summary type="html"><h1 id="工作环境"><a href="#工作环境" class="headerlink" title="工作环境"></a>工作环境</h1><ul>
<li>具备公网 IP 的云服务器(Ubuntu22.04)</li>
<li>黑群晖DS918+</li>
<li>G</summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Linux" scheme="https://xmaihh.github.io/blog/tags/Linux/"/>
<category term="Docker" scheme="https://xmaihh.github.io/blog/tags/Docker/"/>
<category term="docker-compose" scheme="https://xmaihh.github.io/blog/tags/docker-compose/"/>
<category term="Frp" scheme="https://xmaihh.github.io/blog/tags/Frp/"/>
</entry>
<entry>
<title>如何在 sudo 命令中保留特定环境变量</title>
<link href="https://xmaihh.github.io/blog/2024/07/06/ru-he-zai-sudo-ming-ling-zhong-bao-liu-te-ding-huan-jing-bian-liang/"/>
<id>https://xmaihh.github.io/blog/2024/07/06/ru-he-zai-sudo-ming-ling-zhong-bao-liu-te-ding-huan-jing-bian-liang/</id>
<published>2024-07-06T03:52:35.000Z</published>
<updated>2024-07-06T03:52:35.000Z</updated>
<content type="html"><![CDATA[<p>今天在Ubuntu中执行<code>npm install -g hexo-cli</code>时,死活执行不成功。明明设置了当前用户的代理,哪怕在root和普通用户的环境变量中都设了代理,都不行,</p><p>我当前用户的**~/.bashrc**是设置好了代理的:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># set proxy</span></span><br><span class="line"><span class="built_in">export</span> http_proxy=http://proxy.example.com:8080</span><br><span class="line"><span class="built_in">export</span> https_proxy=https://proxy.example.com:8080</span><br><span class="line"><span class="built_in">export</span> no_proxy=<span class="string">"localhost,127.0.0.1"</span></span><br></pre></td></tr></table></figure><p>解决方法是通过编辑 <code>/etc/sudoers</code> 文件来确保特定的环境变量在使用 <code>sudo</code> 时被保留。</p><p>以下是完整的操作步骤示例:</p><ol><li><p>为了安全起见,应该使用 <code>visudo</code> 命令来编辑 <code>/etc/sudoers</code> 文件。这是因为 <code>visudo</code> 会在保存文件之前检查语法,防止语法错误导致权限问题。</p></li><li><p>在打开的文件中添加或修改以下行:</p></li></ol><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Defaults env_keep += <span class="string">"http_proxy https_proxy no_proxy"</span></span><br></pre></td></tr></table></figure><ol start="3"><li><p>保存并退出;</p><p>保存文件并退出编辑器。默认情况下,<code>visudo</code> 使用 <code>nano</code> 作为编辑器,你可以按 <code>Ctrl+X</code> 然后按 <code>Y</code> 并回车保存文件并退出。</p></li></ol><p>验证配置</p><p>使用 <code>sudo</code> 运行一个命令并检查环境变量是否被保留</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">sudo</span> -E <span class="built_in">env</span> | grep -E <span class="string">'http_proxy|https_proxy|no_proxy'</span></span><br></pre></td></tr></table></figure><p>你应该看到输出包含你设置的 <code>http_proxy</code>、<code>https_proxy</code> 和 <code>no_proxy</code> 的值。</p><p>再次执行<code>sudo npm install --unsafe-perm --verbose -g hexo-cli</code>顺利执行,问题解决!</p>]]></content>
<summary type="html"><p>今天在Ubuntu中执行<code>npm install -g hexo-cli</code>时,死活执行不成功。明明设置了当前用户的代理,哪怕在root和普通用户的环境变量中都设了代理,都不行,</p>
<p>我当前用户的**~&#x2F;.bashrc**是设置好了代</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Linux" scheme="https://xmaihh.github.io/blog/tags/Linux/"/>
<category term="Hexo" scheme="https://xmaihh.github.io/blog/tags/Hexo/"/>
<category term="npm" scheme="https://xmaihh.github.io/blog/tags/npm/"/>
</entry>
<entry>
<title>Pixel8保留Magisk进行OTA升级</title>
<link href="https://xmaihh.github.io/blog/2024/06/27/pixel8-bao-liu-magisk-jin-xing-ota-sheng-ji/"/>
<id>https://xmaihh.github.io/blog/2024/06/27/pixel8-bao-liu-magisk-jin-xing-ota-sheng-ji/</id>
<published>2024-06-27T11:25:42.000Z</published>
<updated>2024-06-27T11:25:42.000Z</updated>
<content type="html"><![CDATA[<h1 id="Pixel8保留Magisk进行OTA升级"><a href="#Pixel8保留Magisk进行OTA升级" class="headerlink" title="Pixel8保留Magisk进行OTA升级"></a>Pixel8保留Magisk进行OTA升级</h1><p>每月月初谷歌都会推送系统更新(OTA),更新的时候,会检测验证system分区是否完整,如果被修改,则会导致OTA失败,手机可能变“砖”。刷Magisk时修改了<code>boot.img</code>这个system分区的文件,所以刷Magisk后不能直接安装OTA更新。</p><h1 id="确认-A-B-系统分区支持状态"><a href="#确认-A-B-系统分区支持状态" class="headerlink" title="确认 A/B 系统分区支持状态"></a>确认 A/B 系统分区支持状态</h1><p>A/B 系统分区是 Google 在 Android 7.0 时代引入的新机制,顾名思义,采用这个机制的设备拥有 A、B 两套系统分区,用户数据则能够在这两套系统分区之间共用。A/B 分区同样也是安装了 Magisk 状态下进行无痛 OTA 系统更新的前提条件。</p><h2 id="验证支持系统更新的-A-B-分区"><a href="#验证支持系统更新的-A-B-分区" class="headerlink" title="验证支持系统更新的 A/B 分区"></a>验证支持系统更新的 A/B 分区</h2><p>执行adb shell命令:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb shell</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">getprop ro.build.ab_update</span></span><br></pre></td></tr></table></figure><p>执行 getprop ro.build.ab_update 返回结果为 true 则表示你的设备采用了 A/B 系统分区。</p><h1 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h1><h3 id="关闭-开发者设置中的系统自动更新"><a href="#关闭-开发者设置中的系统自动更新" class="headerlink" title="关闭 开发者设置中的系统自动更新"></a>关闭 开发者设置中的系统自动更新</h3><p>开发者设置 -> 系统自动更新 -> 关闭</p><h3 id="卸载Magisk"><a href="#卸载Magisk" class="headerlink" title="卸载Magisk"></a>卸载Magisk</h3><ol><li>打开应用 <code>magisk</code></li><li>点击<code>卸载Magisk</code></li><li>点击弹出窗口的<code>还原原厂映象</code></li></ol><blockquote><p>提示*<code>stock backup does not exist</code><em>或者</em><code>原厂镜像备份不存在</code>*</p></blockquote><h3 id="解决原厂镜像不存在的问题"><a href="#解决原厂镜像不存在的问题" class="headerlink" title="解决原厂镜像不存在的问题"></a>解决原厂镜像不存在的问题</h3><blockquote><p>如果还原成功则不需要进行这一步操作</p></blockquote><ol><li><p>打开应用<code>设置</code>-><code>关于手机</code>-><code>版本号</code></p><p>我这里的版本号UQ1A.240105.004</p></li><li><p>下载对应的镜像,找到对应<code>手机型号</code>对应的<code>版本号</code></p><p><a href="https://developers.google.com/android/images?hl=zh-cn#coral">下载地址</a></p><p>我这里的手机型号Pixel8,所有我下载的镜像应该是<code>shiba-uq1a.240105.004</code></p></li><li><p>解压<code>boot.img</code></p><p> 在下载好的压缩包找到 <code>image- 开头的压缩包</code> 再从这个<code>image-开头的压缩包</code>里面提取<code>boot.img</code></p></li><li><p>制作准备还原的<code>boot.img</code></p><p>在电脑上<code>push boot.img</code>到手机上</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb push boot.img /sdcard/boot.img</span></span><br></pre></td></tr></table></figure><p>进入<code>adb shell</code></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb shell</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">su</span></span><br></pre></td></tr></table></figure><p>依次执行以下命令</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_"># </span><span class="language-bash">SHA1=$(<span class="built_in">cat</span> $(magisk --path)/.magisk/config | grep SHA1 | <span class="built_in">cut</span> -d <span class="string">'='</span> -f 2)</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">gzip -9f /sdcard/boot.img</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">mkdir</span> /data/magisk_backup_<span class="variable">${SHA1}</span></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">mv</span> /sdcard/boot.img.gz /data/magisk_backup_<span class="variable">${SHA1}</span>/boot.img.gz</span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">chmod</span> -R 755 /data/magisk_backup_<span class="variable">${SHA1}</span></span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash"><span class="built_in">chown</span> -R root.root /data/magisk_backup_<span class="variable">${SHA1}</span></span></span><br></pre></td></tr></table></figure><p>上面的操作都成功了就可以了。</p></li></ol><h3 id="升级系统"><a href="#升级系统" class="headerlink" title="升级系统"></a>升级系统</h3><p>再次尝试升级系统,如果这个时候还是报升级失败的话,只能重启系统后再升级,但是如果可以直接升级了就直接升级,升级完成之后<em><strong>千万别重启</strong></em>。</p><h3 id="升级系统后不重启安装Magisk"><a href="#升级系统后不重启安装Magisk" class="headerlink" title="升级系统后不重启安装Magisk"></a>升级系统后不重启安装Magisk</h3><p>打开应用<code>Magisk</code>->安装Magisk->安装到未使用的槽位(OTA后),完成后重启。</p><h3 id="卸载Magisk还是不能升级系统"><a href="#卸载Magisk还是不能升级系统" class="headerlink" title="卸载Magisk还是不能升级系统"></a>卸载Magisk还是不能升级系统</h3><blockquote><p>以下步骤其实就是重新刷入Magisk,此步骤需要在打开 USB 调试后将手机与电脑相连。</p></blockquote><ol><li><p>重启手机</p></li><li><p>进入系统后打开应用<code>Magisk</code>确认一下Magisk是否安装,如果未安装可以进行以下步骤。</p><p> <img src="https://s2.loli.net/2024/09/02/JP8CR16lkcvQdNx.png" alt="img"></p><p> <strong>Ramdisk</strong>的结果决定了您的设备启动分区中是否有ramdisk。</p><p> 如果您的设备<strong>有</strong>启动 ramdisk,请获取<code>boot.img</code>的副本(或<code>init_boot.img</code> ,如果存在)。</p><p> 如果您的设备<strong>没有</strong>启动 ramdisk,请获取<code>recovery.img</code>的副本。</p> <figure class="highlight 1c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">开始</span><br><span class="line"> <span class="string">|</span></span><br><span class="line"> v</span><br><span class="line">设备是否有启动 ramdisk?</span><br><span class="line"> <span class="string">|</span></span><br><span class="line"> <span class="punctuation">+</span><span class="punctuation">-</span><span class="punctuation">-</span><span class="punctuation">-</span> 是 <span class="punctuation">-</span><span class="punctuation">-</span><span class="punctuation">-</span>> 获取 boot.img <span class="punctuation">(</span>或 init_boot.img<span class="punctuation">)</span></span><br><span class="line"> <span class="string">|</span></span><br><span class="line"> <span class="punctuation">+</span><span class="punctuation">-</span><span class="punctuation">-</span><span class="punctuation">-</span> 否 <span class="punctuation">-</span><span class="punctuation">-</span><span class="punctuation">-</span>> 获取 recovery.img</span><br><span class="line"> <span class="string">|</span></span><br><span class="line"> v</span><br><span class="line">结束</span><br></pre></td></tr></table></figure><p> 相对应的,请使用:</p><p> <code>fastboot flash boot /path/to/magisk_patched_xxx.img</code></p><p> <code>fastboot flash init_boot /path/to/magisk_patched_xxx.img</code></p><p> <code>fastboot flash recovery /path/to/magisk_patched_xxx.img</code>。</p><p> 使用哪个img制作的patched需要刷入到对应分区。</p></li><li><p>直接点击升级系统,等待升级完成,重启系统。</p></li><li><p>下载新系统对应的镜像</p><p>打开应用<code>设置</code>-><code>关于手机</code>-><code>版本号</code></p><p>下载对应的镜像,找到对应<code>手机型号</code>对应的<code>版本号</code></p><p><a href="https://developers.google.com/android/images?hl=zh-cn#coral">下载地址</a></p></li><li><p>解压出boot.img </p></li><li><p>上传boot.img到手机</p> <figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb push boot.img /sdcard/Download/boot.img</span></span><br></pre></td></tr></table></figure></li><li><p>打开<code>Magisk Manager</code> 安装程序</p></li><li><p>制作Magisk镜像并安装</p><p>依次点击<code>安装</code>-> <code>选择并修补一个文件</code>-> 刚刚push到手机上的boot.img 等待执行完成会告诉你文件名</p></li><li><p>pull制作好的Magisk镜像到电脑上</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb pull /sdcard/Download/magisk_pathced_xxx.img ./</span></span><br></pre></td></tr></table></figure></li><li><p>在电脑上刷入magisk</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">adb reboot bootloader</span> </span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">fastboot flash boot /xx/xx/magisk_pathced_xxx.img</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">fastboot reboot</span></span><br></pre></td></tr></table></figure></li></ol><h3 id="执行fastboot一直wait-device的解决办法"><a href="#执行fastboot一直wait-device的解决办法" class="headerlink" title="执行fastboot一直wait device的解决办法"></a>执行fastboot一直wait device的解决办法</h3><p>下载安装usb驱动即可。</p><ol><li>下载驱动<a href="https://dl-ssl.google.com/android/repository/latest_usb_driver_windows.zip">latest_usb_driver_windows</a></li><li>解压到文件</li><li>打开 <code>设备管理器</code> -> <code>操作</code> -> <code>添加驱动程序</code></li><li>选择刚刚解压的usb驱动文件夹确认安装即可。</li><li>再次执行<code>fastboot flash boot</code>命令。</li></ol>]]></content>
<summary type="html"><h1 id="Pixel8保留Magisk进行OTA升级"><a href="#Pixel8保留Magisk进行OTA升级" class="headerlink" title="Pixel8保留Magisk进行OTA升级"></a>Pixel8保留Magisk进行OTA升级</</summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Android" scheme="https://xmaihh.github.io/blog/tags/Android/"/>
<category term="Pixel8" scheme="https://xmaihh.github.io/blog/tags/Pixel8/"/>
<category term="Magisk" scheme="https://xmaihh.github.io/blog/tags/Magisk/"/>
</entry>
<entry>
<title>使用Tailscale+自建DERP组建私有局域网</title>
<link href="https://xmaihh.github.io/blog/2024/06/18/shi-yong-tailscale-zi-jian-derp-zu-jian-si-you-ju-yu-wang/"/>
<id>https://xmaihh.github.io/blog/2024/06/18/shi-yong-tailscale-zi-jian-derp-zu-jian-si-you-ju-yu-wang/</id>
<published>2024-06-18T03:52:35.000Z</published>
<updated>2024-06-18T03:52:35.000Z</updated>
<content type="html"><![CDATA[<p>Tailscale属于一种虚拟组网工具,基于WireGuard。</p><h1 id="注册Tailscale帐号"><a href="#注册Tailscale帐号" class="headerlink" title="注册Tailscale帐号"></a>注册Tailscale帐号</h1><p><strong><a href="https://tailscale.com/">Tailscale官网 https://tailscale.com/</a></strong></p><h1 id="下载客户端"><a href="#下载客户端" class="headerlink" title="下载客户端"></a>下载客户端</h1><p><strong><a href="https://tailscale.com/download/">Tailscale客户端 https://tailscale.com/download/</a></strong></p><p><img src="https://s2.loli.net/2024/06/27/yFap9dfmCGIjswN.png"></p><p><strong>安装并完成登录</strong></p><h1 id="连接起来"><a href="#连接起来" class="headerlink" title="连接起来"></a>连接起来</h1><p>一旦你的设备都装上了Tailscale,它们就像是在一个无形的网里,只属于你的。这时,你的电脑、手机、甚至是那台老旧的平板,都能在这个网络中自由通行,无需那些烦人的防火墙的阻挡。</p><blockquote><p>以上是理想情况,打洞成功与否看各个客户端的网络环境。一般情况打洞成功率比较高,比如手机网络,有IPv6地址的网络。 一般不行:公司有防火墙。 打洞不成功就需要中转节点,客户端之间通过中转连接。那么,你懂的,这些中转节点都在国外,为了提高网络速度,最好自建国内的DERP节点。</p></blockquote><h1 id="架起你的灯塔"><a href="#架起你的灯塔" class="headerlink" title="架起你的灯塔"></a>架起你的灯塔</h1><p>自建DERP服务器。听起来高大上对吧?其实就是让你在这个网络里有自己的标志物,像是海盗的旗帜一样,让你的小伙伴们都能找到你。搭建DERP简单的一行命令(一般人没有备案域名吧)</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run --restart always --net host --name derper -d ghcr.io/yangchuansheng/ip_derper:latest </span><br></pre></td></tr></table></figure><blockquote><p>需要开放防火墙端口或者安全策略,以阿里云为例,添加<strong>12345/tcp</strong>和<strong>3478/udp</strong>安全策略</p></blockquote><h1 id="自由畅游"><a href="#自由畅游" class="headerlink" title="自由畅游"></a>自由畅游</h1><p>配置ACL</p><p><img src="https://s2.loli.net/2024/06/27/jvMTJOYIFUCPyqg.png"></p><p>输入配置信息:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line">"derpMap": {</span><br><span class="line">"OmitDefaultRegions": false, // 可以设置为 true,这样不会下发官方的 derper 节点,测试或者实际使用都可以考虑打开</span><br><span class="line">"Regions": {</span><br><span class="line">"900": {</span><br><span class="line">"RegionID": 900, // tailscale 900-999 是保留给自定义 derper 的</span><br><span class="line">"RegionCode": "you region code",</span><br><span class="line">"RegionName": "you region code",</span><br><span class="line">"Nodes": [</span><br><span class="line">{</span><br><span class="line">"Name": "vps-1",</span><br><span class="line">"RegionID": 900,</span><br><span class="line">"IPv4": "xxx.xxx.xxx.xxx", # 你的VPS 公网IP地址</span><br><span class="line">// "DERPPort": 4430,</span><br><span class="line">"InsecureForTests": true, // 因为是自签名证书,所以客户端不做校验</span><br><span class="line">},</span><br><span class="line">],</span><br><span class="line">},</span><br><span class="line">},</span><br><span class="line">},</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>现在,不管是在家里,公司,还是在世界的另一端,只要你的设备一连上Tailscale,那么,恭喜你,你就进入了自己的秘密花园。在这里,没有限制,只有自由。</p><p><img src="https://s2.loli.net/2024/06/27/SlhbuQkzAHUOGse.png"></p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://www.xiaoiluo.com/article/tailscale-derp">https://www.xiaoiluo.com/article/tailscale-derp</a></p><p><a href="https://icloudnative.io/posts/custom-derp-servers/">https://icloudnative.io/posts/custom-derp-servers/</a></p>]]></content>
<summary type="html"><p>Tailscale属于一种虚拟组网工具,基于WireGuard。</p>
<h1 id="注册Tailscale帐号"><a href="#注册Tailscale帐号" class="headerlink" title="注册Tailscale帐号"></a>注册Tails</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Linux" scheme="https://xmaihh.github.io/blog/tags/Linux/"/>
<category term="Tailscale" scheme="https://xmaihh.github.io/blog/tags/Tailscale/"/>
</entry>
<entry>
<title>Linux中安装配置docker管理器Portainer&Portainer升级版本</title>
<link href="https://xmaihh.github.io/blog/2024/04/19/linux-zhong-an-zhuang-pei-zhi-docker-guan-li-qi-portainer-portainer-sheng-ji-ban-ben/"/>
<id>https://xmaihh.github.io/blog/2024/04/19/linux-zhong-an-zhuang-pei-zhi-docker-guan-li-qi-portainer-portainer-sheng-ji-ban-ben/</id>
<published>2024-04-19T13:25:36.000Z</published>
<updated>2024-04-19T13:25:36.000Z</updated>
<content type="html"><![CDATA[<h1 id="单机部署"><a href="#单机部署" class="headerlink" title="单机部署"></a>单机部署</h1><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker pull portainer/portainer-ce</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker volume create portainer_data</span></span><br><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -d -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce</span></span><br></pre></td></tr></table></figure><ul><li>打开浏览器,输入<code>http://localhost:9000</code>)或者<code>http://{服务器ip}:9000</code>;</li><li>设置用户名和密码后进入</li></ul><h1 id="Portainer升级版本"><a href="#Portainer升级版本" class="headerlink" title="Portainer升级版本"></a>Portainer升级版本</h1><p> 在 Portainer 安装时候是指定了数据卷的,这样一来,更新 Portainer 只需要下载新的 Portainer 的镜像,删除原有容器即可,原先的记录信息都在数据卷中。</p><p>1.关闭容器</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker stop portainer的容器名或容器Id</span></span><br></pre></td></tr></table></figure><p>2.删除容器</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker <span class="built_in">rm</span> portainer的容器名或容器<span class="built_in">id</span></span></span><br></pre></td></tr></table></figure><p>3.确定下容器是否已经删除</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker ps -a</span></span><br></pre></td></tr></table></figure><p>4.删除镜像</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker rmi portainer的镜像名或镜像Id</span></span><br></pre></td></tr></table></figure><p>5.拉取新版本镜像</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker pull portainer/portainer-ce</span></span><br></pre></td></tr></table></figure><p>6.启动镜像,打开浏览器输入原有帐号密码即可</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">$ </span><span class="language-bash">docker run -d -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce</span></span><br></pre></td></tr></table></figure><p>Portainer升级版本完整的操作日志记录</p><figure class="highlight makefile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">ubuntu@ubuntu:~$docker ps</span></span><br><span class="line">CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span><br><span class="line">fc281d660e7e portainer/portainer-ce <span class="string">"/portainer"</span> 2 weeks ago Up 22 hours 8000/tcp, 9443/tcp, 0.0.0.0:9000->9000/tcp, :::9000->9000/tcp portainer</span><br><span class="line"><span class="section">ubuntu@ubuntu:~$ docker stop fc281d660e7e</span></span><br><span class="line">fc281d660e7e</span><br><span class="line"><span class="section">ubuntu@ubuntu:~$ docker ps -a</span></span><br><span class="line">CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES</span><br><span class="line">fc281d660e7e portainer/portainer-ce <span class="string">"/portainer"</span> 2 weeks ago Exited (2) About a minute ago portainer</span><br><span class="line"><span class="section">ubuntu@ubuntu:~$ docker rm fc281d660e7e</span></span><br><span class="line">fc281d660e7e</span><br><span class="line"><span class="section">ubuntu@ubuntu:~$ docker images</span></span><br><span class="line">REPOSITORY TAG IMAGE ID CREATED SIZE</span><br><span class="line">portainer/portainer-ce latest 9281e1907542 17 months ago 278MB</span><br><span class="line"><span class="section">ubuntu@ubuntu:~$ docker rmi 9281e1907542</span></span><br><span class="line"><span class="section">Untagged: portainer/portainer-ce:latest</span></span><br><span class="line"><span class="section">Untagged: portainer/portainer-ce@sha256:47b064434edf437badf7337e516e07f64477485c8ecc663ddabbe824b20c672d</span></span><br><span class="line"><span class="section">Deleted: sha256:9281e1907542d9e135476db62e7dd129a95972dc5cd297f5d01acff58c4f751f</span></span><br><span class="line"><span class="section">Deleted: sha256:067f72a72d633747ba5a6039a1b4ec3d36555fa22a07f6e5c3be2940d4d040cc</span></span><br><span class="line"><span class="section">Deleted: sha256:f6fe101531bcf0e63b651a4e3ce2676c1a7f1880288bb288ede04fc1deb1a8a1</span></span><br><span class="line"><span class="section">Deleted: sha256:e0a46f5d05e1b93a7993c45aaea39729d111d7a096e02ac1656c721e39cb5222</span></span><br><span class="line"><span class="section">Deleted: sha256:8c004456aeb58b75f792fa091b194c20d6ed4f0d95dd25b0150d71c5c9ab601b</span></span><br><span class="line"><span class="section">ubuntu@ubuntu:~$ docker pull portainer/portainer-ce</span></span><br><span class="line">Using default tag: latest</span><br><span class="line"><span class="section">latest: Pulling from portainer/portainer-ce</span></span><br><span class="line"><span class="section">57654d40e0a5: Pull complete</span></span><br><span class="line"><span class="section">1f476acfabd6: Pull complete</span></span><br><span class="line"><span class="section">23f2184d3136: Pull complete</span></span><br><span class="line"><span class="section">e21d017187f1: Pull complete</span></span><br><span class="line"><span class="section">bfa9cfee4c8e: Pull complete</span></span><br><span class="line"><span class="section">9d8366b4fa62: Pull complete</span></span><br><span class="line"><span class="section">d55f4e10dc55: Pull complete</span></span><br><span class="line"><span class="section">5230628c9a1d: Pull complete</span></span><br><span class="line"><span class="section">dd27a37dee51: Pull complete</span></span><br><span class="line"><span class="section">5cc1bbad4ed2: Pull complete</span></span><br><span class="line"><span class="section">4f4fb700ef54: Pull complete</span></span><br><span class="line"><span class="section">Digest: sha256:4a1ceadd7f7898d9190ee0a6d22234c4323aefd80e796e84f5e57127f74370f1</span></span><br><span class="line"><span class="section">Status: Downloaded newer image for portainer/portainer-ce:latest</span></span><br><span class="line"><span class="section">docker.io/portainer/portainer-ce:latest</span></span><br><span class="line"><span class="section">ubuntu@ubuntu:~$ docker run -d -p 9000:9000 --name=portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce</span></span><br><span class="line">d6e582a7f0646ae628e15b4ee9a69f85c276d29b78cc23636b219d0f65b82f89</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html"><h1 id="单机部署"><a href="#单机部署" class="headerlink" title="单机部署"></a>单机部署</h1><figure class="highlight shell"><table><tr><td class="gutter"><pr</summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Linux" scheme="https://xmaihh.github.io/blog/tags/Linux/"/>
<category term="Docker" scheme="https://xmaihh.github.io/blog/tags/Docker/"/>
<category term="Portainer" scheme="https://xmaihh.github.io/blog/tags/Portainer/"/>
</entry>
<entry>
<title>Android中下载文件到SD卡的Download文件夹</title>
<link href="https://xmaihh.github.io/blog/2024/03/24/android-zhong-xia-zai-wen-jian-dao-sd-qia-de-download-wen-jian-jia/"/>
<id>https://xmaihh.github.io/blog/2024/03/24/android-zhong-xia-zai-wen-jian-dao-sd-qia-de-download-wen-jian-jia/</id>
<published>2024-03-24T01:40:55.000Z</published>
<updated>2024-03-24T01:40:55.000Z</updated>
<content type="html"><![CDATA[<h1 id="存储空间"><a href="#存储空间" class="headerlink" title="存储空间"></a>存储空间</h1><p>应用保存数据的方式:</p><ul><li><strong>应用专属存储空间</strong>:存储仅供应用使用的文件,可以存储到内部存储卷中的专属目录或外部存储空间中的其他专属目录。使用内部存储空间中的目录保存其他应用不应访问的敏感信息。</li><li><strong>共享存储</strong>:存储您的应用打算与其他应用共享的文件,包括媒体、文档和其他文件。</li><li><strong>偏好设置</strong>:以键值对形式通过SharePreference存储私有原始数据。</li><li><strong>数据库</strong>:使用 Room 持久性库将结构化数据存储在Sqlite数据库中。</li></ul><p>下表汇总了这些选项的特点:</p><table><thead><tr><th></th><th>内容类型</th><th>访问方法</th><th>所需权限</th><th>其他应用是否可以访问?</th><th>卸载应用时是否移除文件?</th></tr></thead><tbody><tr><td><a href="https://developer.android.com/training/data-storage/app-specific?hl=zh-cn">应用专属文件</a></td><td>仅供您的应用使用的文件</td><td>- 从内部存储空间访问,可以使用 <code>getFilesDir()</code> 或 <code>getCacheDir()</code> 方法 ;- 从外部存储空间访问,可以使用 <code>getExternalFilesDir()</code> 或 <code>getExternalCacheDir()</code> 方法</td><td>从内部存储空间访问不需要任何权限;如果应用在搭载 Android 4.4(API 级别 19)或更高版本的设备上运行,从外部存储空间访问不需要任何权限</td><td>否</td><td>是</td></tr><tr><td><a href="https://developer.android.com/training/data-storage/shared/media?hl=zh-cn">媒体</a></td><td>可共享的媒体文件(图片、音频文件、视频)</td><td><code>MediaStore</code> API</td><td>在 Android 11(API 级别 30)或更高版本中,访问其他应用的文件需要 <code>READ_EXTERNAL_STORAGE</code>;在 Android 10(API 级别 29)中,访问其他应用的文件需要 <code>READ_EXTERNAL_STORAGE</code> 或 <code>WRITE_EXTERNAL_STORAGE</code>;在 Android 9(API 级别 28)或更低版本中,访问<strong>所有</strong>文件均需要相关权限</td><td>是,但其他应用需要 <code>READ_EXTERNAL_STORAGE</code> 权限</td><td>否</td></tr><tr><td><a href="https://developer.android.com/training/data-storage/shared/documents-files?hl=zh-cn">文档和其他文件</a></td><td>其他类型的可共享内容,包括已下载的文件</td><td>存储访问框架SAF</td><td>无</td><td>是,可以通过系统文件选择器访问</td><td>否</td></tr><tr><td><a href="https://developer.android.com/training/data-storage/shared-preferences?hl=zh-cn">应用偏好设置</a></td><td>键值对</td><td><a href="https://developer.android.com/guide/topics/ui/settings/use-saved-values?hl=zh-cn">Jetpack Preferences</a> 库</td><td>无</td><td>否</td><td>是</td></tr><tr><td>数据库</td><td>结构化数据</td><td><a href="https://developer.android.com/training/data-storage/room?hl=zh-cn">Room</a> 持久性库</td><td>无</td><td>否</td><td>否</td></tr></tbody></table><p><strong>外部存储</strong></p><p>以前的手机是存在SDcard的,但目前很多手机都取消了SDcard,Android上引入了映射机制来创建虚拟的SDcard,我们通过文件管理器看到的路径<code>storage/emulated/0</code>就是虚拟SDcard,也就是我们俗称的“外部存储空间”或者“公共存储空间”<strong>app申请的读写权限请求都是申请的外部存储空间权限</strong></p><p><strong>内部存储</strong></p><p>内存存储也就是本app的专属目录,其他app是无法访问的,适合存储敏感文件。<br>通过系统api访问到的路径:/data/user/0/app_packageName/…<br>对应的真实目录:/data/date/app_packageName/…</p><p><strong>分区存储</strong></p><p>分区存储实际上就是外部存储空间中建一个app对应目录,本app无须申请权限就可以访问,如果申请读写权限,意味着申请外部空间所有访问权限,在Android10之前,分区存储目录是有可能被其他app访问到的。从Android11开始,其他app是无法访问的,也称之为沙盒模式。</p><h1 id="权限的变化"><a href="#权限的变化" class="headerlink" title="权限的变化"></a>权限的变化</h1><p><strong>Android 5.0 存储访问框架(SAF)</strong>:引入了存储访问框架,用于提供一种更加安全和细粒度的文件访问方式。通过 SAF,用户可以选择授予应用访问特定文件或目录的权限。</p><p><strong>Android 6.0 运行时权限</strong>:引入了运行时权限模型,应用需要在运行时动态请求存储权限,而不是在安装时获得。这进一步提高了用户的控制权。</p><p><strong>Android 7.0 File URI 限制</strong>:文件URI被限制,应用不能直接通过<code>file://</code>访问其他应用的文件,必须使用<code>ContentProvider</code>来共享文件。</p><p><strong>Android 10</strong></p><p><strong>分区存储(Scoped Storage)</strong>:引入了分区存储,限制了应用对外部存储的访问。应用只能访问自己的应用专属目录和一些特定的公共目录(如<code>Download</code>、<code>Pictures</code>等)。</p><p>媒体文件访问权限:应用可以通过特定的媒体存储API访问和操作媒体文件(如图片、音频、视频),而不需要全局存储权限。</p><p><strong>Android 11</strong></p><p>进一步收紧分区存储:分区存储变得更加严格,应用对外部存储的访问进一步受限。</p><p><strong>MANAGE_EXTERNAL_STORAGE 权限</strong>:该权限提供对应用专属目录和 <code>MediaStore</code> 之外文件的写入权限。引入了新的权限,允许某些应用访问所有的外部存储文件,但这个权限的使用受到严格限制,需要通过Google Play审核。</p><p>媒体存储访问权限:应用可以请求访问特定类型的媒体文件,更加细粒度的访问控制。</p><p><strong>Android 12</strong></p><p>特定作用域的媒体访问:为不同类型的媒体文件提供更细致的访问控制,实现了更细粒度的权限管理。</p><p><strong>Android 13</strong> 与<strong>Android 14</strong> 倒是没有对权限进行大的改动,目前已经趋于稳定。</p><h1 id="下载文件到SD卡的Download文件夹"><a href="#下载文件到SD卡的Download文件夹" class="headerlink" title="下载文件到SD卡的Download文件夹"></a>下载文件到SD卡的Download文件夹</h1><h2 id="方案一:申请权限写入"><a href="#方案一:申请权限写入" class="headerlink" title="方案一:申请权限写入"></a>方案一:申请权限写入</h2><p>需要在AndroidManifest.xml中添加以下权限:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">"android.permission.INTERNET"</span> /></span></span><br><span class="line"><span class="tag"><<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">"android.permission.WRITE_EXTERNAL_STORAGE"</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">android:maxSdkVersion</span>=<span class="string">"28"</span> /></span></span><br></pre></td></tr></table></figure><p>对于Android 10(API 29)及以上版本:</p><ul><li>使用MediaStore API来创建文件。</li><li>使用ContentResolver来获取输出流并写入文件。</li></ul><p>对于Android 9(API 28)及以下版本:</p><ul><li>直接使用File API在Download目录创建文件。</li><li>使用FileOutputStream写入文件。</li></ul><p>使用线程进行网络操作和文件写入,避免阻塞主线程。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.content.ContentValues</span><br><span class="line"><span class="keyword">import</span> android.net.Uri</span><br><span class="line"><span class="keyword">import</span> android.os.Build</span><br><span class="line"><span class="keyword">import</span> android.os.Environment</span><br><span class="line"><span class="keyword">import</span> android.provider.MediaStore</span><br><span class="line"><span class="keyword">import</span> androidx.appcompat.app.AppCompatActivity</span><br><span class="line"><span class="keyword">import</span> java.io.File</span><br><span class="line"><span class="keyword">import</span> java.io.FileOutputStream</span><br><span class="line"><span class="keyword">import</span> java.io.InputStream</span><br><span class="line"><span class="keyword">import</span> java.net.HttpURLConnection</span><br><span class="line"><span class="keyword">import</span> java.net.URL</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">DownloadActivity</span> : <span class="type">AppCompatActivity</span>() {</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">downloadFile</span><span class="params">(url: <span class="type">String</span>, fileName: <span class="type">String</span>)</span></span> {</span><br><span class="line"> <span class="keyword">val</span> thread = Thread {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">val</span> connection = URL(url).openConnection() <span class="keyword">as</span> HttpURLConnection</span><br><span class="line"> connection.connect()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (connection.responseCode != HttpURLConnection.HTTP_OK) {</span><br><span class="line"> <span class="keyword">return</span><span class="symbol">@Thread</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> inputStream: InputStream = connection.inputStream</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {</span><br><span class="line"> <span class="comment">// Android 10及以上版本</span></span><br><span class="line"> <span class="keyword">val</span> contentValues = ContentValues().apply {</span><br><span class="line"> put(MediaStore.Downloads.DISPLAY_NAME, fileName)</span><br><span class="line"> put(MediaStore.Downloads.MIME_TYPE, <span class="string">"application/octet-stream"</span>)</span><br><span class="line"> put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">val</span> resolver = contentResolver</span><br><span class="line"> <span class="keyword">val</span> uri = resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)</span><br><span class="line"> <span class="keyword">if</span> (uri != <span class="literal">null</span>) {</span><br><span class="line"> resolver.openOutputStream(uri)?.use { outputStream -></span><br><span class="line"> inputStream.copyTo(outputStream)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Android 9及以下版本</span></span><br><span class="line"> <span class="keyword">val</span> downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)</span><br><span class="line"> <span class="keyword">val</span> file = File(downloadsDir, fileName)</span><br><span class="line"> FileOutputStream(file).use { outputStream -></span><br><span class="line"> inputStream.copyTo(outputStream)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> inputStream.close()</span><br><span class="line"> } <span class="keyword">catch</span> (e: Exception) {</span><br><span class="line"> e.printStackTrace()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> thread.start()</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>注意,WRITE_EXTERNAL_STORAGE权限只在API 28及以下版本需要。对于API 29+,我们使用MediaStore API,不需要这个权限。<br>最后,记得在运行时请求WRITE_EXTERNAL_STORAGE权限(对于Android 6.0到Android 9.0)。</p></blockquote><h2 id="方案二:无需权限SAF选择目录写入"><a href="#方案二:无需权限SAF选择目录写入" class="headerlink" title="方案二:无需权限SAF选择目录写入"></a>方案二:无需权限SAF选择目录写入</h2><p>通过 Storage Access Framework (SAF) 先获取一个目录的 URI使用 DocumentFile 来实现文件下载。</p><p>这个方法的优点包括:</p><ol><li>兼容性好:适用于所有 Android 版本。</li><li>支持可移动存储:用户可以选择 SD 卡上的目录。</li><li>无需特殊权限:不需要 WRITE_EXTERNAL_STORAGE 权限。</li><li>用户友好:用户可以选择他们想要保存文件的位置。</li></ol><p>使用这种方法时,需要注意以下几点:</p><ol><li>需要用户选择一个目录,这可能会增加一些用户交互。</li><li>你需要保存用户选择的目录 URI,以便在后续的下载中重用。</li><li>对于大文件,你可能需要考虑添加进度回调和取消功能。</li></ol><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.content.Context</span><br><span class="line"><span class="keyword">import</span> android.os.Build</span><br><span class="line"><span class="keyword">import</span> android.os.Environment</span><br><span class="line"><span class="keyword">import</span> androidx.documentfile.provider.DocumentFile</span><br><span class="line"><span class="keyword">import</span> java.io.BufferedInputStream</span><br><span class="line"><span class="keyword">import</span> java.io.BufferedOutputStream</span><br><span class="line"><span class="keyword">import</span> java.io.File</span><br><span class="line"><span class="keyword">import</span> java.net.HttpURLConnection</span><br><span class="line"><span class="keyword">import</span> java.net.URL</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">DocumentFileDownloader</span>(<span class="keyword">private</span> <span class="keyword">val</span> context: Context) {</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">downloadFile</span><span class="params">(url: <span class="type">String</span>, fileName: <span class="type">String</span>)</span></span> {</span><br><span class="line"> Thread {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">val</span> file = getOrCreateFile(fileName)</span><br><span class="line"> <span class="keyword">if</span> (file == <span class="literal">null</span>) {</span><br><span class="line"> println(<span class="string">"Could not create or access file"</span>)</span><br><span class="line"> <span class="keyword">return</span><span class="symbol">@Thread</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打开连接并下载</span></span><br><span class="line"> <span class="keyword">val</span> connection = URL(url).openConnection() <span class="keyword">as</span> HttpURLConnection</span><br><span class="line"> connection.connect()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (connection.responseCode != HttpURLConnection.HTTP_OK) {</span><br><span class="line"> <span class="keyword">throw</span> Exception(<span class="string">"Server returned HTTP <span class="subst">${connection.responseCode}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打开输入流和输出流</span></span><br><span class="line"> <span class="keyword">val</span> inputStream = BufferedInputStream(connection.inputStream)</span><br><span class="line"> <span class="keyword">val</span> outputStream = BufferedOutputStream(context.contentResolver.openOutputStream(file.uri))</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 复制数据</span></span><br><span class="line"> <span class="keyword">val</span> buffer = ByteArray(<span class="number">8192</span>)</span><br><span class="line"> <span class="keyword">var</span> bytesRead: <span class="built_in">Int</span></span><br><span class="line"> <span class="keyword">while</span> (inputStream.read(buffer).also { bytesRead = it } != -<span class="number">1</span>) {</span><br><span class="line"> outputStream.write(buffer, <span class="number">0</span>, bytesRead)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 关闭流</span></span><br><span class="line"> outputStream.flush()</span><br><span class="line"> outputStream.close()</span><br><span class="line"> inputStream.close()</span><br><span class="line"></span><br><span class="line"> println(<span class="string">"File downloaded successfully: <span class="subst">${file.uri}</span>"</span>)</span><br><span class="line"> } <span class="keyword">catch</span> (e: Exception) {</span><br><span class="line"> e.printStackTrace()</span><br><span class="line"> }</span><br><span class="line"> }.start()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">getOrCreateFile</span><span class="params">(fileName: <span class="type">String</span>)</span></span>: DocumentFile? {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {</span><br><span class="line"> <span class="comment">// Android 10 及以上版本</span></span><br><span class="line"> <span class="keyword">val</span> downloadsDir = DocumentFile.fromTreeUri(context, </span><br><span class="line"> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toUri())</span><br><span class="line"> downloadsDir?.createFile(<span class="string">"application/octet-stream"</span>, fileName)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Android 9 及以下版本</span></span><br><span class="line"> <span class="keyword">val</span> downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)</span><br><span class="line"> <span class="keyword">val</span> file = File(downloadsDir, fileName)</span><br><span class="line"> DocumentFile.fromFile(file)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如何使用这个下载器的示例:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MainActivity</span> : <span class="type">AppCompatActivity</span>() {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">lateinit</span> <span class="keyword">var</span> downloader: DocumentFileDownloader</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">var</span> directoryUri: Uri? = <span class="literal">null</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"> setContentView(R.layout.activity_main)</span><br><span class="line"></span><br><span class="line"> downloader = DocumentFileDownloader(<span class="keyword">this</span>)</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 假设你有一个按钮来触发文件选择</span></span><br><span class="line"> findViewById<Button>(R.id.selectFolderButton).setOnClickListener {</span><br><span class="line"> selectFolder()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 假设你有一个按钮来触发下载</span></span><br><span class="line"> findViewById<Button>(R.id.downloadButton).setOnClickListener {</span><br><span class="line"> directoryUri?.let {</span><br><span class="line"> downloader.downloadFile(</span><br><span class="line"> <span class="string">"https://example.com/file.zip"</span>,</span><br><span class="line"> <span class="string">"downloaded_file.zip"</span>,</span><br><span class="line"> it</span><br><span class="line"> )</span><br><span class="line"> } ?: run {</span><br><span class="line"> Toast.makeText(<span class="keyword">this</span>, <span class="string">"Please select a folder first"</span>, Toast.LENGTH_SHORT).show()</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">selectFolder</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">val</span> intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)</span><br><span class="line"> startActivityForResult(intent, OPEN_DIRECTORY_REQUEST_CODE)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onActivityResult</span><span class="params">(requestCode: <span class="type">Int</span>, resultCode: <span class="type">Int</span>, <span class="keyword">data</span>: <span class="type">Intent</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onActivityResult(requestCode, resultCode, <span class="keyword">data</span>)</span><br><span class="line"> <span class="keyword">if</span> (requestCode == OPEN_DIRECTORY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {</span><br><span class="line"> <span class="keyword">data</span>?.<span class="keyword">data</span>?.let { uri -></span><br><span class="line"> contentResolver.takePersistableUriPermission(</span><br><span class="line"> uri,</span><br><span class="line"> Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION</span><br><span class="line"> )</span><br><span class="line"> directoryUri = uri</span><br><span class="line"> <span class="comment">// 可以保存这个 URI 以便后续使用</span></span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">companion</span> <span class="keyword">object</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">val</span> OPEN_DIRECTORY_REQUEST_CODE = <span class="number">1</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="方案三:无需权限无需选择目录-DocumentFile-API写入"><a href="#方案三:无需权限无需选择目录-DocumentFile-API写入" class="headerlink" title="方案三:无需权限无需选择目录 DocumentFile API写入"></a>方案三:无需权限无需选择目录 DocumentFile API写入</h2><p>希望使用 DocumentFile API 直接创建指定的文件,而不需要用户选择目录。</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.content.Context</span><br><span class="line"><span class="keyword">import</span> android.os.Build</span><br><span class="line"><span class="keyword">import</span> android.os.Environment</span><br><span class="line"><span class="keyword">import</span> androidx.documentfile.provider.DocumentFile</span><br><span class="line"><span class="keyword">import</span> java.io.BufferedInputStream</span><br><span class="line"><span class="keyword">import</span> java.io.BufferedOutputStream</span><br><span class="line"><span class="keyword">import</span> java.io.File</span><br><span class="line"><span class="keyword">import</span> java.net.HttpURLConnection</span><br><span class="line"><span class="keyword">import</span> java.net.URL</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">DocumentFileDownloader</span>(<span class="keyword">private</span> <span class="keyword">val</span> context: Context) {</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">downloadFile</span><span class="params">(url: <span class="type">String</span>, fileName: <span class="type">String</span>)</span></span> {</span><br><span class="line"> Thread {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">val</span> file = getOrCreateFile(fileName)</span><br><span class="line"> <span class="keyword">if</span> (file == <span class="literal">null</span>) {</span><br><span class="line"> println(<span class="string">"Could not create or access file"</span>)</span><br><span class="line"> <span class="keyword">return</span><span class="symbol">@Thread</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打开连接并下载</span></span><br><span class="line"> <span class="keyword">val</span> connection = URL(url).openConnection() <span class="keyword">as</span> HttpURLConnection</span><br><span class="line"> connection.connect()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (connection.responseCode != HttpURLConnection.HTTP_OK) {</span><br><span class="line"> <span class="keyword">throw</span> Exception(<span class="string">"Server returned HTTP <span class="subst">${connection.responseCode}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打开输入流和输出流</span></span><br><span class="line"> <span class="keyword">val</span> inputStream = BufferedInputStream(connection.inputStream)</span><br><span class="line"> <span class="keyword">val</span> outputStream = BufferedOutputStream(context.contentResolver.openOutputStream(file.uri))</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 复制数据</span></span><br><span class="line"> <span class="keyword">val</span> buffer = ByteArray(<span class="number">8192</span>)</span><br><span class="line"> <span class="keyword">var</span> bytesRead: <span class="built_in">Int</span></span><br><span class="line"> <span class="keyword">while</span> (inputStream.read(buffer).also { bytesRead = it } != -<span class="number">1</span>) {</span><br><span class="line"> outputStream.write(buffer, <span class="number">0</span>, bytesRead)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 关闭流</span></span><br><span class="line"> outputStream.flush()</span><br><span class="line"> outputStream.close()</span><br><span class="line"> inputStream.close()</span><br><span class="line"></span><br><span class="line"> println(<span class="string">"File downloaded successfully: <span class="subst">${file.uri}</span>"</span>)</span><br><span class="line"> } <span class="keyword">catch</span> (e: Exception) {</span><br><span class="line"> e.printStackTrace()</span><br><span class="line"> }</span><br><span class="line"> }.start()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">getOrCreateFile</span><span class="params">(fileName: <span class="type">String</span>)</span></span>: DocumentFile? {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {</span><br><span class="line"> <span class="comment">// Android 10 及以上版本</span></span><br><span class="line"> <span class="keyword">val</span> downloadsDir = DocumentFile.fromTreeUri(context, </span><br><span class="line"> Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toUri())</span><br><span class="line"> downloadsDir?.createFile(<span class="string">"application/octet-stream"</span>, fileName)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// Android 9 及以下版本</span></span><br><span class="line"> <span class="keyword">val</span> downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)</span><br><span class="line"> <span class="keyword">val</span> file = File(downloadsDir, fileName)</span><br><span class="line"> DocumentFile.fromFile(file)</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个实现有以下几个特点:</p><ol><li>它直接使用公共下载目录,不需要用户选择目录。</li><li>对于 Android 10 及以上版本,它使用 DocumentFile API 来创建文件。</li><li>对于 Android 9 及以下版本,它直接在下载目录创建文件,然后将其包装为 DocumentFile。</li></ol><p>使用这个下载器的方式如下:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MainActivity</span> : <span class="type">AppCompatActivity</span>() {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">lateinit</span> <span class="keyword">var</span> downloader: DocumentFileDownloader</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"> setContentView(R.layout.activity_main)</span><br><span class="line"></span><br><span class="line"> downloader = DocumentFileDownloader(<span class="keyword">this</span>)</span><br><span class="line"></span><br><span class="line"> findViewById<Button>(R.id.downloadButton).setOnClickListener {</span><br><span class="line"> downloader.downloadFile(</span><br><span class="line"> <span class="string">"https://example.com/file.zip"</span>,</span><br><span class="line"> <span class="string">"downloaded_file.zip"</span></span><br><span class="line"> )</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>需要注意的是:</p><ol><li><p>对于 Android 9 及以下版本,你仍然需要 WRITE_EXTERNAL_STORAGE 权限。你可以在 AndroidManifest.xml 中这样声明:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">"android.permission.INTERNET"</span> /></span></span><br><span class="line"><span class="tag"><<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">"android.permission.WRITE_EXTERNAL_STORAGE"</span> </span></span><br><span class="line"><span class="tag"> <span class="attr">android:maxSdkVersion</span>=<span class="string">"28"</span> /></span></span><br></pre></td></tr></table></figure></li><li><p>对于 Android 6.0 到 Android 9.0,你需要在运行时请求 WRITE_EXTERNAL_STORAGE 权限。</p></li><li><p>这种方法在 Android 10 及以上版本不需要特殊权限,因为它使用了 MediaStore API 来访问下载目录。</p></li><li><p>如果你的应用目标 SDK 是 Android 10 或更高,并且你想要访问应用专用目录之外的文件,你可能需要在 AndroidManifest.xml 中添加 <code>android:requestLegacyExternalStorage="true"</code>。但请注意,这只是一个临时解决方案,在 Android 11 及以上版本中不再有效。</p></li></ol><p>关于<strong>DocumentFile</strong> 和<strong>权限</strong>的情况:</p><p>Android 10(API 29)及以上:</p><ul><li>使用 DocumentFile 访问应用专属目录或通过 SAF(Storage Access Framework)访问的目录时,不需要额外的存储权限。</li><li>对于下载目录等公共目录,也不需要 WRITE_EXTERNAL_STORAGE 权限。</li></ul><p>Android 9(API 28)及以下:</p><ul><li>如果使用 DocumentFile.fromFile() 来访问外部存储上的文件,仍然需要 WRITE_EXTERNAL_STORAGE 权限。</li></ul><p>考虑到这一点,我们可以优化之前的代码,使其在<strong>所有 Android 版本上都不需要请求 WRITE_EXTERNAL_STORAGE 权限</strong>。我们可以使用 MediaStore API 来实现这一点:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> android.content.ContentValues</span><br><span class="line"><span class="keyword">import</span> android.content.Context</span><br><span class="line"><span class="keyword">import</span> android.net.Uri</span><br><span class="line"><span class="keyword">import</span> android.os.Build</span><br><span class="line"><span class="keyword">import</span> android.os.Environment</span><br><span class="line"><span class="keyword">import</span> android.provider.MediaStore</span><br><span class="line"><span class="keyword">import</span> java.io.BufferedInputStream</span><br><span class="line"><span class="keyword">import</span> java.io.BufferedOutputStream</span><br><span class="line"><span class="keyword">import</span> java.net.HttpURLConnection</span><br><span class="line"><span class="keyword">import</span> java.net.URL</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">FileDownloader</span>(<span class="keyword">private</span> <span class="keyword">val</span> context: Context) {</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">fun</span> <span class="title">downloadFile</span><span class="params">(url: <span class="type">String</span>, fileName: <span class="type">String</span>)</span></span> {</span><br><span class="line"> Thread {</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">val</span> uri = getOutputUri(fileName)</span><br><span class="line"> <span class="keyword">if</span> (uri == <span class="literal">null</span>) {</span><br><span class="line"> println(<span class="string">"Could not create output URI"</span>)</span><br><span class="line"> <span class="keyword">return</span><span class="symbol">@Thread</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打开连接并下载</span></span><br><span class="line"> <span class="keyword">val</span> connection = URL(url).openConnection() <span class="keyword">as</span> HttpURLConnection</span><br><span class="line"> connection.connect()</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (connection.responseCode != HttpURLConnection.HTTP_OK) {</span><br><span class="line"> <span class="keyword">throw</span> Exception(<span class="string">"Server returned HTTP <span class="subst">${connection.responseCode}</span>"</span>)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 打开输入流和输出流</span></span><br><span class="line"> <span class="keyword">val</span> inputStream = BufferedInputStream(connection.inputStream)</span><br><span class="line"> <span class="keyword">val</span> outputStream = BufferedOutputStream(context.contentResolver.openOutputStream(uri))</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 复制数据</span></span><br><span class="line"> <span class="keyword">val</span> buffer = ByteArray(<span class="number">8192</span>)</span><br><span class="line"> <span class="keyword">var</span> bytesRead: <span class="built_in">Int</span></span><br><span class="line"> <span class="keyword">while</span> (inputStream.read(buffer).also { bytesRead = it } != -<span class="number">1</span>) {</span><br><span class="line"> outputStream.write(buffer, <span class="number">0</span>, bytesRead)</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 关闭流</span></span><br><span class="line"> outputStream.flush()</span><br><span class="line"> outputStream.close()</span><br><span class="line"> inputStream.close()</span><br><span class="line"></span><br><span class="line"> println(<span class="string">"File downloaded successfully: <span class="variable">$uri</span>"</span>)</span><br><span class="line"> } <span class="keyword">catch</span> (e: Exception) {</span><br><span class="line"> e.printStackTrace()</span><br><span class="line"> }</span><br><span class="line"> }.start()</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="function"><span class="keyword">fun</span> <span class="title">getOutputUri</span><span class="params">(fileName: <span class="type">String</span>)</span></span>: Uri? {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {</span><br><span class="line"> <span class="keyword">val</span> contentValues = ContentValues().apply {</span><br><span class="line"> put(MediaStore.Downloads.DISPLAY_NAME, fileName)</span><br><span class="line"> put(MediaStore.Downloads.MIME_TYPE, <span class="string">"application/octet-stream"</span>)</span><br><span class="line"> put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)</span><br><span class="line"> }</span><br><span class="line"> context.contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues)</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">val</span> downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)</span><br><span class="line"> Uri.fromFile(java.io.File(downloadsDir, fileName))</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个实现有以下优点:</p><ol><li>它在所有 Android 版本上都不需要 WRITE_EXTERNAL_STORAGE 权限。</li><li>对于 Android 10 及以上版本,它使用 MediaStore API 来创建文件。</li><li>对于 Android 9 及以下版本,它仍然使用 File API,但通过 ContentResolver 来写入文件,这样不需要存储权限。</li></ol><p>使用这个下载器的方式如下:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">MainActivity</span> : <span class="type">AppCompatActivity</span>() {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">lateinit</span> <span class="keyword">var</span> downloader: FileDownloader</span><br><span class="line"></span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">onCreate</span><span class="params">(savedInstanceState: <span class="type">Bundle</span>?)</span></span> {</span><br><span class="line"> <span class="keyword">super</span>.onCreate(savedInstanceState)</span><br><span class="line"> setContentView(R.layout.activity_main)</span><br><span class="line"></span><br><span class="line"> downloader = FileDownloader(<span class="keyword">this</span>)</span><br><span class="line"></span><br><span class="line"> findViewById<Button>(R.id.downloadButton).setOnClickListener {</span><br><span class="line"> downloader.downloadFile(</span><br><span class="line"> <span class="string">"https://example.com/file.zip"</span>,</span><br><span class="line"> <span class="string">"downloaded_file.zip"</span></span><br><span class="line"> )</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在 AndroidManifest.xml 中,你只需要声明网络权限:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">uses-permission</span> <span class="attr">android:name</span>=<span class="string">"android.permission.INTERNET"</span> /></span></span><br></pre></td></tr></table></figure><p>这种方法的优点是:</p><ol><li>兼容性好:适用于所有 Android 版本。</li><li>不需要存储权限:在所有版本上都不需要 WRITE_EXTERNAL_STORAGE 权限。</li><li>简单直接:不需要用户选择目录或处理 SAF。</li></ol><p>需要注意的是,这种方法将文件下载到公共的下载目录。如果你需要将文件保存到应用的私有目录,或者需要更多的文件操作灵活性,可能还是需要考虑使用 SAF 或其他方法。</p><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://juejin.cn/post/6987569764407181349">聊一聊Android存储行为的变化</a></p><p><a href="https://juejin.cn/post/7383311950175272998">就想下载个文件到SD卡,怎就这么难?快把代码拿走吧</a></p>]]></content>
<summary type="html"><h1 id="存储空间"><a href="#存储空间" class="headerlink" title="存储空间"></a>存储空间</h1><p>应用保存数据的方式:</p>
<ul>
<li><strong>应用专属存储空间</strong>:存储仅供应用使用的文件,</summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Android" scheme="https://xmaihh.github.io/blog/tags/Android/"/>
</entry>
<entry>
<title>使用 Github Actions 编译Python项目自动化构建 exe</title>
<link href="https://xmaihh.github.io/blog/2024/01/18/shi-yong-github-actions-bian-yi-python-xiang-mu-zi-dong-hua-gou-jian-exe/"/>
<id>https://xmaihh.github.io/blog/2024/01/18/shi-yong-github-actions-bian-yi-python-xiang-mu-zi-dong-hua-gou-jian-exe/</id>
<published>2024-01-18T14:15:28.000Z</published>
<updated>2024-01-18T14:15:28.000Z</updated>
<content type="html"><![CDATA[<p><a href="https://github.com/features/actions">GitHub Actions</a> 是 GitHub 的持续集成服务。</p><p>在这个Python项目 <a href="https://github.com/xmaihh/CSVFilter"><strong>https://github.com/xmaihh/CSVFilter</strong> </a> 中使用Github Actions自动化构建exe并发布到Release。</p><p><strong>pyinstaller使用问题</strong>参考**<a href="https://github.com/HaujetZhao/PyInstaller-Perfect-Build-Method">PyInstaller-Perfect-Build-Method</a>**</p><p>在每个工作流作业开始时,GitHub 会自动创建唯一的 GITHUB_TOKEN 机密以在工作流中使用。 可以使用 GITHUB_TOKEN 在工作流作业中进行身份验证。使用标准语法引用密钥<code>GITHUB_TOKEN:${{ secrets.GITHUB_TOKEN }}</code>,有关详细信息请参阅“<a href="https://docs.github.com/zh/actions/security-guides/automatic-token-authentication#about-the-github_token-secret">关于 <code>GITHUB_TOKEN</code> 机密</a>”。</p><h1 id="使用-Github-Actions-编译Python项目自动化构建-exe"><a href="#使用-Github-Actions-编译Python项目自动化构建-exe" class="headerlink" title="使用 Github Actions 编译Python项目自动化构建 exe"></a>使用 Github Actions 编译Python项目自动化构建 exe</h1><p>在你的<strong>Python源码仓库</strong>根目录下创建一个 <strong>.github/workflows</strong> 文件夹,在该文件夹内创建一个新的 <strong>YAML</strong> 文件(例如 deploy.yml)用于定义 <strong>GitHub Actions</strong> 工作流。</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># This workflow will upload a Python Package using PyInstaller when a release is published</span></span><br><span class="line"><span class="comment"># For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python</span></span><br><span class="line"></span><br><span class="line"><span class="attr">name:</span> <span class="string">Publish</span> <span class="string">to</span> <span class="string">Release</span></span><br><span class="line"></span><br><span class="line"><span class="attr">on:</span></span><br><span class="line"> <span class="attr">push:</span></span><br><span class="line"> <span class="attr">branches:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="string">main</span></span><br><span class="line"> <span class="attr">workflow_dispatch:</span></span><br><span class="line"></span><br><span class="line"><span class="attr">jobs:</span></span><br><span class="line"> <span class="attr">build:</span></span><br><span class="line"> <span class="attr">runs-on:</span> <span class="string">windows-latest</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">OUTPUT_FILE_PREFIX:</span> <span class="string">CSVFilter.Setup</span></span><br><span class="line"></span><br><span class="line"> <span class="attr">steps:</span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Checkout</span> <span class="string">repository</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/checkout@v3</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">fetch-depth:</span> <span class="number">0</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Set</span> <span class="string">up</span> <span class="string">Python</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/setup-python@v4</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">python-version:</span> <span class="string">'3.12'</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Install</span> <span class="string">dependencies</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> python -m pip install --upgrade pip</span></span><br><span class="line"><span class="string"> pip install pip-tools</span></span><br><span class="line"><span class="string"> pip-sync requirements.txt</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Update</span> <span class="string">configuration</span> <span class="string">file</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">python</span> <span class="string">prebuild_scripts/version.py</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Read</span> <span class="string">config</span> <span class="string">file</span></span><br><span class="line"> <span class="attr">id:</span> <span class="string">read_config</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> $version = (python -c "import configparser;config = configparser.ConfigParser();config.read('config.ini');print(config.get('DEFAULT', 'version'))")</span></span><br><span class="line"><span class="string"> echo "version=$version" >> $env:GITHUB_OUTPUT</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Build</span> <span class="string">executable</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> echo "Using version: ${{ steps.read_config.outputs.version }}"</span></span><br><span class="line"><span class="string"> pyinstaller --onefile --add-data "resources;resources" --add-data "config.ini;." --icon="resources/csv_filter.ico" --windowed --clean --name "${{ env.OUTPUT_FILE_PREFIX }}.${{ steps.read_config.outputs.version }}" main.py</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Create</span> <span class="string">release</span></span><br><span class="line"> <span class="attr">id:</span> <span class="string">create_release</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/create-release@v1</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">GITHUB_TOKEN:</span> <span class="string">${{</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">tag_name:</span> <span class="string">v${{</span> <span class="string">steps.read_config.outputs.version</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">release_name:</span> <span class="string">Release</span> <span class="string">${{</span> <span class="string">steps.read_config.outputs.version</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">draft:</span> <span class="literal">false</span></span><br><span class="line"> <span class="attr">prerelease:</span> <span class="literal">false</span></span><br><span class="line"></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Delete</span> <span class="string">old</span> <span class="string">releases</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">GITHUB_TOKEN:</span> <span class="string">${{</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">run:</span> <span class="string">|</span></span><br><span class="line"><span class="string"> $current_release = "v${{ steps.read_config.outputs.version }}"</span></span><br><span class="line"><span class="string"> $releases = gh release list --limit 1000 | ForEach-Object { $_.Split()[0] }</span></span><br><span class="line"><span class="string"> foreach ($release in $releases) {</span></span><br><span class="line"><span class="string"> if ($release -ne $current_release) {</span></span><br><span class="line"><span class="string"> echo "Deleting release: $release"</span></span><br><span class="line"><span class="string"> gh release delete $release --cleanup-tag -y</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"> }</span></span><br><span class="line"><span class="string"></span></span><br><span class="line"> <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Upload</span> <span class="string">release</span> <span class="string">asset</span></span><br><span class="line"> <span class="attr">uses:</span> <span class="string">actions/upload-release-asset@v1</span></span><br><span class="line"> <span class="attr">env:</span></span><br><span class="line"> <span class="attr">GITHUB_TOKEN:</span> <span class="string">${{</span> <span class="string">secrets.GITHUB_TOKEN</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">with:</span></span><br><span class="line"> <span class="attr">upload_url:</span> <span class="string">${{</span> <span class="string">steps.create_release.outputs.upload_url</span> <span class="string">}}</span></span><br><span class="line"> <span class="attr">asset_path:</span> <span class="string">dist/${{</span> <span class="string">env.OUTPUT_FILE_PREFIX</span> <span class="string">}}.${{</span> <span class="string">steps.read_config.outputs.version</span> <span class="string">}}.exe</span></span><br><span class="line"> <span class="attr">asset_name:</span> <span class="string">${{</span> <span class="string">env.OUTPUT_FILE_PREFIX</span> <span class="string">}}.${{</span> <span class="string">steps.read_config.outputs.version</span> <span class="string">}}.exe</span></span><br><span class="line"> <span class="attr">asset_content_type:</span> <span class="string">application/octet-stream</span></span><br></pre></td></tr></table></figure><h1 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h1><p><a href="https://docs.github.com/en/actions">官方文档</a></p><p><strong><a href="https://github.com/HaujetZhao/PyInstaller-Perfect-Build-Method">PyInstaller-Perfect-Build-Method</a></strong></p>]]></content>
<summary type="html"><p><a href="https://github.com/features/actions">GitHub Actions</a> 是 GitHub 的持续集成服务。</p>
<p>在这个Python项目 <a href="https://github.com/xmaihh/</summary>
<category term="学习笔记" scheme="https://xmaihh.github.io/blog/categories/%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/"/>
<category term="Github Actions" scheme="https://xmaihh.github.io/blog/tags/Github-Actions/"/>
<category term="CI/CD" scheme="https://xmaihh.github.io/blog/tags/CI-CD/"/>
<category term="Python" scheme="https://xmaihh.github.io/blog/tags/Python/"/>
</entry>
<entry>
<title>linux下如何更好地使用</title>
<link href="https://xmaihh.github.io/blog/2024/01/09/linux-xia-ru-he-geng-hao-di-shi-yong/"/>
<id>https://xmaihh.github.io/blog/2024/01/09/linux-xia-ru-he-geng-hao-di-shi-yong/</id>
<published>2024-01-08T16:55:12.000Z</published>
<updated>2024-01-08T16:55:12.000Z</updated>
<content type="html"><![CDATA[<p>[TOC]</p><h1 id="常规安全更新"><a href="#常规安全更新" class="headerlink" title="常规安全更新"></a>常规安全更新</h1><p>通过 <code>unattended-upgrades</code>,可以使 Ubuntu 系统自动进行常规的安全相关更新,使系统一直保持 security。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install unattended-upgrades</span><br><span class="line">sudo dpkg-reconfigure unattended-upgrades</span><br></pre></td></tr></table></figure><h1 id="更改SSH端口"><a href="#更改SSH端口" class="headerlink" title="更改SSH端口"></a>更改SSH端口</h1><p>默认情况下,SSH 服务器侦听端口 22。出于安全原因,许多系统管理员选择将此默认端口更改为另一个不太可预测的数字,以帮助防止自动攻击。</p><p>分步指南:</p><ol><li><p>备份配置文件。在进行任何更改之前,最好备份 SSH 配置文件。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup</span><br></pre></td></tr></table></figure></li><li><p>编辑 SSH 配置文件。使用您喜欢的文本编辑器打开 SSHD 配置文件。在此示例中,我们将使用 nano。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo nano /etc/ssh/sshd_config</span><br></pre></td></tr></table></figure></li><li><p>找到端口指令。找到以 Port 开头的行。默认情况下,它应该显示端口 22。</p></li><li><p>更改端口号。编辑该行以反映所需的端口号,最好高于 1024,以避免与其他标准服务发生冲突。例如,要将其更改为端口 2222,该行将如下所示:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Port 2222</span><br></pre></td></tr></table></figure></li><li><p>保存并关闭文件。如果您使用的是 nano,请按 CTRL + O 写入更改,然后按 Enter 键和 CTRL + X 退出。</p></li><li><p>调整防火墙规则。如果启用了防火墙(如 UFW 或 Firewalld),则需要更新其规则以允许在新的 SSH 端口上进行连接。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo ufw allow 2222/tcp</span><br></pre></td></tr></table></figure></li><li><p>重新启动 SSH 服务。通过重新启动 SSH 守护程序来应用更改。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl restart sshd</span><br></pre></td></tr></table></figure></li><li><p>测试新的 SSH 端口。在注销当前会话之前,请打开新的终端或 SSH 客户端,并尝试使用新端口连接到服务器,以确保一切正常:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ssh username@your_server_ip -p 2222</span><br></pre></td></tr></table></figure></li></ol><h1 id="linux下创建新用户"><a href="#linux下创建新用户" class="headerlink" title="linux下创建新用户"></a>linux下创建新用户</h1><p>Linux下新建用户需要使用<code>useradd</code>和<code>passwd</code>命令</p><ul><li><p>最基础的使用方法</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">useradd <username> # 新建一个名为<username>的用户</span><br><span class="line">passwd <username> <password> # 为该用户设定密码为<password></span><br></pre></td></tr></table></figure></li><li><p>最佳实践</p><p>新建用户有时候我们需要连带着完成一些其他的目的,例如修改用户的默认shell(默认的/bin/sh功能太少,甚至不能使用方向键和Tab键)以及为新用户指定home目录的位置,于是我们可以这么使用</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">useradd -d <homedir> -m -s /bin/bash <username></span><br></pre></td></tr></table></figure><p>对上述命令的翻译:<br>新建用户<code><username></code><br><code>-s</code>:指定shell到<code>/bin/bash</code><br><code>-d</code>:指定其home目录为<code><homedir></code><br><code>-m</code>:如果指定的home目录不存在,则新建</p></li></ul><p><code>useradd</code> 可用来建立用户帐号。帐号建好之后,再用 <code>passwd</code> 设定帐号的密码。而可用 <code>userdel</code> 删除帐号。使用 <code>useradd</code> 指令所建立的帐号,实际上是保存在 <code>/etc/passwd</code> 文本文件中。</p><p>参数说明:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line">Usage: useradd [options] LOGIN</span><br><span class="line"> useradd -D</span><br><span class="line"> useradd -D [options]</span><br><span class="line"></span><br><span class="line">Options:</span><br><span class="line"> --badnames 对用户名不做检测</span><br><span class="line"> -b, --base-dir BASE_DIR 为新建的用户指定home目录到BASE_DIR下</span><br><span class="line"> --btrfs-subvolume-home use BTRFS subvolume for home directory</span><br><span class="line"> -c, --comment COMMENT 加上备注文字。备注文字会保存在passwd的备注栏位中</span><br><span class="line"> -d, --home-dir HOME_DIR 指定home目录</span><br><span class="line"> -D, --defaults 变更预设值</span><br><span class="line"> -e, --expiredate EXPIRE_DATE 新用户账户的过期时间</span><br><span class="line"> -f, --inactive INACTIVE 指定在密码过期后多少天即关闭该帐号</span><br><span class="line"> -g, --gid GROUP 为新用户指定用户组(组名或者GID)</span><br><span class="line"> -G, --groups GROUPS 指定用户所属的附加群组</span><br><span class="line"> -h, --help 显示帮助信息</span><br><span class="line"> -k, --skel SKEL_DIR use this alternative skeleton directory</span><br><span class="line"> -K, --key KEY=VALUE override /etc/login.defs defaults</span><br><span class="line"> -l, --no-log-init do not add the user to the lastlog and</span><br><span class="line"> faillog databases</span><br><span class="line"> -m, --create-home 为此用户创建home目录</span><br><span class="line"> -M, --no-create-home 不为此用户创建home目录</span><br><span class="line"> -N, --no-user-group 不为此用户创建同名的用户组</span><br><span class="line"> -o, --non-unique allow to create users with duplicate</span><br><span class="line"> (non-unique) UID</span><br><span class="line"> -p, --password PASSWORD encrypted password of the new account</span><br><span class="line"> -r, --system create a system account</span><br><span class="line"> -R, --root CHROOT_DIR directory to chroot into</span><br><span class="line"> -P, --prefix PREFIX_DIR prefix directory where are located the /etc/* files</span><br><span class="line"> -s, --shell SHELL 为新用户指定shell(例如/bin/bash)</span><br><span class="line"> -u, --uid UID 为新用户指定UID</span><br><span class="line"> -U, --user-group create a group with the same name as the user</span><br><span class="line"> -Z, --selinux-user SEUSER use a specific SEUSER for the SELinux user mapping</span><br><span class="line"> --extrausers Use the extra users database</span><br></pre></td></tr></table></figure><h1 id="向用户授予sudo权限"><a href="#向用户授予sudo权限" class="headerlink" title="向用户授予sudo权限"></a>向用户授予sudo权限</h1><p>在 Linux 机器中登录 ssh 后,我收到以下消息,执行一个sudo命令,得到提示“’username’ is not in the sudoers file. This incident will be reported.”解决这个问题的办法是,向用户授予 sudo 权限。</p><p>假设 <code>/etc/sudoers</code> 文件被更改以防止 sudo 或 admin 组中的用户将其权限提升为超级用户的权限,则对 sudoers 文件进行备份,如下所示:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">cp /etc/sudoers /etc/sudoers.orginal</span><br></pre></td></tr></table></figure><p>打开 sudoers 文件</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">visudo</span><br></pre></td></tr></table></figure><p>然后在管理员用户下方添加用户,如下语法所示:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">username ALL=(ALL) ALL</span><br></pre></td></tr></table></figure><p>上面的语法每次执行sudo指令是需要passwd的,如果需要免密码,则需按如下语法所示:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">username ALL=(ALL) NOPASSWD:ALL</span><br></pre></td></tr></table></figure><h1 id="使用-fail2ban-保护您的系统"><a href="#使用-fail2ban-保护您的系统" class="headerlink" title="使用 fail2ban 保护您的系统"></a>使用 fail2ban 保护您的系统</h1><p>注意:Fail2ban 只能用于保护需要用户名/密码身份验证的服务。例如,您无法使用 fail2ban 保护 ping。</p><p>在本文中,我将演示保护 SSH 守护程序 (SSHD) 免受暴力攻击。您可以设置过滤器,就像 <code>fail2ban</code> 它们一样,以保护系统上的几乎所有收听服务。</p><ul><li><p>安装和初始设置</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install fail2ban -y</span><br><span class="line">sudo systemctl enable fail2ban</span><br><span class="line">sudo systemctl start fail2ban</span><br></pre></td></tr></table></figure><p>除非您的 <code>fail2ban</code> 配置中存在某种语法问题,否则您不会看到任何标准输出消息。</p></li><li><p>基础配置</p><p>现在配置一些基本的东西 <code>fail2ban</code> 来保护系统,而不会干扰它自己。将 <code>/etc/fail2ban/jail.conf</code> 文件复制到 <code>/etc/fail2ban/jail.local</code> 。该 <code>jail.local</code> 文件是我们感兴趣的配置文件。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local</span><br></pre></td></tr></table></figure><p>默认情况下,fail2ban 附带一个 <code>jail.conf</code> 文件。但是,这可能会在更新中被覆盖,因此您应该将此文件复制到 <code>jail.local</code> 文件中并在其中进行调整。</p><p>如果您已有文件 <code>jail.local</code> ,请使用 <code>nano</code> 或您喜欢的文本编辑器打开它:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo nano /etc/fail2ban/jail.local</span><br></pre></td></tr></table></figure><p>每次进行配置更改时都必须重新启动 <code>fail2ban</code> </p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl restart fail2ban</span><br></pre></td></tr></table></figure></li><li><p>设置筛选服务</p><p>要设置过滤服务,必须在 <code>/etc/fail2ban/jail.d</code> 目录下创建相应的“jail”文件。对于 SSHD,创建一个名为 <code>sshd.local</code> 的新文件,并在其中输入服务过滤指令。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">[sshd]</span><br><span class="line">enabled = true</span><br><span class="line">backend= systemd</span><br><span class="line">port = ssh</span><br><span class="line">banaction = ufw[application=$(app), blocktype=reject]</span><br><span class="line">logpath = /var/log/sshd/error.log</span><br><span class="line">maxretry = 3</span><br><span class="line">findtime = 120</span><br><span class="line">bantime = 600</span><br></pre></td></tr></table></figure><p>大多数设置都是不言自明的。对上述命令的翻译, 在连续的120s内错误输入密码三次以上的用户在 600 秒(10 分钟)禁止违规系统的 IP 地址。</p><p>重新启动 <code>fail2ban</code> 服务。每次进行配置更改时都必须重新启动 <code>fail2ban</code> </p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl restart fail2ban</span><br></pre></td></tr></table></figure></li><li><p>封禁是什么样子的</p><p>查看封禁日志</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tail /var/log/fail2ban.log</span><br></pre></td></tr></table></figure></li><li><p>解除封禁</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo fail2ban-client set sshd unbanip 192.168.1.69</span><br></pre></td></tr></table></figure><p>发出此命令后,立刻解除对<code>192.168.1.69</code>的封禁,无需重新启动 fail2ban 守护程序。</p></li></ul><h1 id="安装docker、docker-compose和Portainer-CE"><a href="#安装docker、docker-compose和Portainer-CE" class="headerlink" title="安装docker、docker-compose和Portainer CE"></a>安装docker、docker-compose和Portainer CE</h1><h2 id="安装docker"><a href="#安装docker" class="headerlink" title="安装docker"></a>安装docker</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://get.docker.com -o get-docker.sh</span><br><span class="line">sh get-docker.sh</span><br></pre></td></tr></table></figure><p>验证安装</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker -v</span><br></pre></td></tr></table></figure><p>若要创建 <code>docker</code> 组并添加用户,请执行以下操作:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo groupadd docker</span><br><span class="line">sudo usermod -aG docker $USER</span><br><span class="line">newgrp docker</span><br></pre></td></tr></table></figure><p>验证是否可以 <code>docker</code> 在不运行 <code>sudo</code></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run hello-world</span><br></pre></td></tr></table></figure><p>此命令下载测试映像并在容器中运行它。当容器运行时,它会打印一条消息并退出。</p><h2 id="安装docker-compose"><a href="#安装docker-compose" class="headerlink" title="安装docker-compose"></a>安装docker-compose</h2><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose</span><br></pre></td></tr></table></figure><p>赋予可执行权限给下载的二进制文件</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo chmod +x /usr/local/bin/docker-compose</span><br></pre></td></tr></table></figure><p>创建一个符号链接,将<code>docker-compose</code>命令链接到<code>/usr/bin</code>目录,以便可以全局访问</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose</span><br></pre></td></tr></table></figure><p>验证安装</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker-compose --version</span><br></pre></td></tr></table></figure><h2 id="安装Portainer-CE"><a href="#安装Portainer-CE" class="headerlink" title="安装Portainer CE"></a>安装Portainer CE</h2><p>创建Portainer将用于存储其数据库的卷</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker volume create portainer_data</span><br></pre></td></tr></table></figure><p>下载 Portainer容器</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker pull portainer/portainer-ce:latest</span><br></pre></td></tr></table></figure><p>启动镜像</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">docker run -d -p 9000:9000 --name portainer --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce:latest</span><br></pre></td></tr></table></figure><p>打开 Web 浏览器 <a href="https://localhost:9000/">https://localhost:9000</a></p><h1 id="安装Tailscale"><a href="#安装Tailscale" class="headerlink" title="安装Tailscale"></a>安装Tailscale</h1><p>安装 Tailscale</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">curl -fsSL https://tailscale.com/install.sh | sh</span><br></pre></td></tr></table></figure><p>将您的机器连接到 Tailscale 网络并在浏览器中进行身份验证:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo tailscale up</span><br></pre></td></tr></table></figure><p>您已连接!您可以通过运行以下命令找到您的 Tailscale IPv4 地址:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tailscale ip -4</span><br></pre></td></tr></table></figure><p>如果添加的设备是服务器或远程访问的设备,则可能需要考虑<a href="https://tailscale.com/kb/1028/key-expiry">禁用密钥过期</a>,以防止需要定期重新进行身份验证。</p><h1 id=""><a href="#" class="headerlink" title=""></a></h1>]]></content>
<summary type="html"><p>[TOC]</p>
<h1 id="常规安全更新"><a href="#常规安全更新" class="headerlink" title="常规安全更新"></a>常规安全更新</h1><p>通过 <code>unattended-upgrades</code>,可以使 U</summary>
<category term="技术分享" scheme="https://xmaihh.github.io/blog/categories/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/"/>
<category term="Linux" scheme="https://xmaihh.github.io/blog/tags/Linux/"/>
<category term="Docker" scheme="https://xmaihh.github.io/blog/tags/Docker/"/>
</entry>
</feed>