Compare commits

..

202 Commits

Author SHA1 Message Date
26a267b111 Editable content rating for every community moderator 2025-04-05 12:06:11 +08:00
1de0a86074 Referenced external attachment 2025-04-05 11:51:35 +08:00
048535d1c0 🐛 Fix analyzer did not clear cache 2025-04-05 11:40:33 +08:00
11d54c7c78 List attachments now return with userinfo 2025-03-31 00:45:39 +08:00
370ee84b34 🛂 Authorized required to access large file 2025-03-30 22:29:57 +08:00
35e4f9a9ad 🐛 Fix image proxy doesn't work 2025-03-30 21:57:30 +08:00
f306ee2e1e Image proxy preview 2025-03-30 21:28:24 +08:00
820d7a9f42 🚚 Expose the models to public 2025-03-29 22:17:22 +08:00
8f91649d25 DirectAccess in filekit 2025-03-29 17:56:38 +08:00
aa52aa73fe Remove useless cache tags 2025-03-29 17:08:17 +08:00
a12ba060f5 ⬆️ Upgrade nexus to fix panic 2025-03-29 16:01:20 +08:00
db0f679a01 Clean up go mod 2025-03-29 15:51:37 +08:00
073b32aa73 ♻️ Rebuilt cache with nexus cache 2025-03-29 15:48:04 +08:00
1390f26afa ♻️ Refactor get s3 client process 2025-03-24 22:48:43 +08:00
9360d17706 Auto to set bucket lookup method 2025-03-24 22:46:23 +08:00
1a1b9eb556 Allow to set preferred storage destination 2025-03-24 21:39:21 +08:00
cd7d19db68 👽 Support Multi-currency 2025-03-23 18:00:27 +08:00
2d4034d7f8 🐛 Try to optimize the uploading 2025-03-23 02:38:20 +08:00
347b1bad77 🐛 Fix large JWT headers 2025-03-23 00:08:32 +08:00
2b87b1db01 ⬆️ Upgrade passport 2025-03-15 17:25:31 +08:00
24ce52902c ⚗️ Disable auto clean up unused attachments for testing 2025-03-11 00:15:51 +08:00
1d866f317f Development SDK of Paperclip
 Support update usage by random id
2025-03-10 23:11:12 +08:00
38aa06cc00 Unused attachment cleanup 2025-03-10 22:30:22 +08:00
e3ca50c4ae Update usage grpc call 2025-03-10 22:15:59 +08:00
69be460e13 🐛 Fix deletion handle 2025-03-10 21:43:43 +08:00
070fd7bb6a 👽 Update the account deletion 2025-03-01 14:07:19 +08:00
10f75ab37a Support webp and apng analyze 2025-02-24 21:15:14 +08:00
671b3fa5a3 🐛 Fix wrong way to determine own pack or not 2025-02-23 13:07:34 +08:00
333aee6b45 Get stickers when get sticker pack 2025-02-23 12:54:27 +08:00
b7a06fabb3 🐛 Fix sticker pack api stacking issue 2025-02-23 12:20:07 +08:00
1dae405232 Sticker pack ownership 2025-02-23 12:02:55 +08:00
cfd1735aee 🐛 Fix panic 2025-02-22 12:34:40 +08:00
0913521c13 🐛 Fix count uploaded bytes incorrectly 2025-02-20 21:08:44 +08:00
aa0f4d292e Billing status displaying 2025-02-18 23:18:38 +08:00
8b02381392 ♻️ Update billing algorithm 2025-02-18 23:12:51 +08:00
22070a464b 🐛 Fix did not preload pack when listing 2025-02-04 02:21:49 +08:00
9ea1012f19 Configurable discount size 2025-01-29 19:14:37 +08:00
80739eab52 Pay for upload 2025-01-29 19:12:54 +08:00
96f429e1d9 🔊 Verbose grpc logging 2025-01-26 23:55:51 +08:00
f0d6cb9499 🔊 Add verbose logging 2025-01-26 21:35:47 +08:00
8b9479833e 🐛 Fix grpc endpoint 2025-01-26 20:45:22 +08:00
0dcbd90f1e :hammer Optimize dockerfile 2025-01-25 00:29:09 +08:00
7c3334a57a Filter grpc endpoints with user 2025-01-24 17:21:28 +08:00
2f256a4c3e 🐛 Fix compile errors 2025-01-24 00:58:39 +08:00
143afdb885 Internal grpc apis 2025-01-24 00:54:16 +08:00
1a9e670d3d 🐛 Fix analyzer again... 2025-01-21 20:07:49 +08:00
4cecbd8e4b 🐛 Fix analyzer calculating ratio 2025-01-21 19:55:41 +08:00
ed9b1474fb 🐛 Fix analyzer panic 2025-01-21 19:11:40 +08:00
ac347540b8 🐛 Analyzer now can handle EXIF Orientation 2025-01-21 12:27:30 +08:00
d0d14d7c67 🐛 Fix analyzer did not save the hash code 2025-01-19 01:44:47 +08:00
8bcf02fa5e Able to directly open (get the image) of a sticker 2025-01-06 21:51:26 +08:00
8cd1037759 🐛 Fix analyze result did not applied 2025-01-01 17:20:30 +08:00
5e73d9acd4 ♻️ Use update api instead of overhaul in background tasks 2025-01-01 11:43:54 +08:00
2bd8dc17d1 🐛 Fix analyze now query issue 2024-12-29 23:19:26 +08:00
78e554577e 🐛 Prevent user from creating boost on same destination 2024-12-29 12:24:56 +08:00
a58f44d50e 🐛 Only apply active boost 2024-12-29 12:23:41 +08:00
c6944cd3df 🐛 Bug fixes 2024-12-29 02:13:17 +08:00
2f1385676b :typo: Fix config typo 2024-12-29 02:13:08 +08:00
af8d665418 🐛 Fix opener get wrong dest 2024-12-29 02:08:41 +08:00
1955d94414 🐛 Fix on boost reupload 2024-12-29 01:59:57 +08:00
0e0ad9c261 🐛 Fix more older cache issue 2024-12-29 01:58:26 +08:00
d979f85f68 🐛 Fix re-upload still need in temp 2024-12-29 01:44:54 +08:00
00fddfdef9 Manually active boost api 2024-12-29 01:38:48 +08:00
3c9b826ed2 🔊 Verbose logging on active boost 2024-12-29 01:36:52 +08:00
b7bedb8dfc Index in list destination api 2024-12-29 01:20:56 +08:00
e89f149336 🐛 Fix cache issue 2024-12-29 01:03:22 +08:00
b041ce3e06 🐛 Fix attachment did not updated 2024-12-29 00:50:04 +08:00
3eee9755d4 Fix listing destinations api 2024-12-29 00:21:29 +08:00
9f0bce81ce 🐛 Fix infinite loading 2024-12-29 00:15:59 +08:00
3a00a06541 🐛 Fix destination loading 2024-12-29 00:03:00 +08:00
cd0141f5b1 🐛 Fix panic 2024-12-28 23:56:02 +08:00
5749b5bfb1 🐛 Fix map routes panic 2024-12-28 23:51:17 +08:00
90fb9960cc Support pre-signed url 2024-12-28 23:16:07 +08:00
ec0444b35c Only can create boost on enabled destinations 2024-12-28 23:12:19 +08:00
49a8159e35 ♻️ Refactored opener
 Support boost in opener
2024-12-28 23:10:57 +08:00
8888f7661a Able to list destinations 2024-12-28 22:27:39 +08:00
ebc3a6f09c List boost by users 2024-12-28 22:14:30 +08:00
1a5787d3c2 List boosts by attachment 2024-12-28 22:08:45 +08:00
b73f5e1912 🎨 Re-orgnize api index 2024-12-28 22:05:05 +08:00
d59966a03e Boost CRUD API 2024-12-28 21:41:13 +08:00
edbe412f97 Casedase deleting attachment 2024-12-28 20:08:32 +08:00
a4e81cabec 🐛 Bug fixes 2024-12-28 19:26:46 +08:00
626fe47bb4 🐛 Fix cannot identify the multipart is completed upload 2024-12-28 16:54:30 +08:00
e297b1b4ea 🐛 Fix backward compability 2024-12-28 16:37:23 +08:00
0c33c2d86c Preload thumbnail, compressed 2024-12-28 15:45:31 +08:00
845894f12c ♻️ Updated way to setting thumbnail & compressed 2024-12-28 15:42:19 +08:00
dda85eae98 🚚 Move io related functions to fs internal package 2024-12-28 14:07:53 +08:00
99dd7f55e0 Fragment uploading continue 2024-12-28 13:56:25 +08:00
30097256b6 🐛 Bug fixes on fragment based uploading 2024-12-28 13:38:55 +08:00
1c8454a658 💥 Change multipart upload API to fragment one 2024-12-28 13:32:08 +08:00
957a9b64b7 ♻️ Fragment based multipart upload API 2024-12-28 13:31:31 +08:00
044b0eefca Able to analyze right now 2024-12-26 22:55:16 +08:00
790b123410 🐛 Fix backward compability 2024-12-26 22:41:11 +08:00
303f8a36f1 Able to edit thumbnail on update meta api 2024-12-26 22:35:39 +08:00
243dbfa9c2 🐛 Fix is indexable query filter issue 2024-12-26 22:11:34 +08:00
3a77af23cb ♻️ Usermeta, is indexable, thumbnail and more fields on attachments 2024-12-26 21:53:09 +08:00
5bbf13b01e 🐛 Fix main file 2024-12-25 23:54:54 +08:00
66ee6c37f2 🐛 Fix delete file load config issue 2024-12-25 23:52:54 +08:00
ae6815b2a4 🐛 Trying fix deleting attachment issue 2024-12-25 23:52:00 +08:00
411b2f3313 🗑️ Remove some files in repo 2024-12-14 12:40:20 +08:00
041bdc6c4d 🐛 Bug fixes on re-uploader 2024-11-10 01:04:07 +08:00
9ed9e4d510 🐛 Fix attachment uploader 2024-11-10 01:00:54 +08:00
d0927b5528 🐛 Fix check reupload condition 2024-11-10 00:59:20 +08:00
6cda5751df 🐛 Still reading the abandoned temporary dest config 2024-11-10 00:50:47 +08:00
c41f8b77ce 🐛 Provide a way to bypass nexus and serving content to prevent nexus reverse proxy issue 2024-11-06 23:01:29 +08:00
2f99ac7b6e 💥 Simplified api of multipart uploading 2024-11-05 20:56:53 +08:00
4223eb6ecd 🐛 Fix hash bug 2024-11-03 11:40:21 +08:00
3f9df23491 🐛 Bug fixes 2024-11-03 02:14:56 +08:00
2d119826fe 🗑️ Remove account 2024-11-03 01:53:57 +08:00
94b777a3e4 🚚 Move from hydrogen to hypernet 2024-11-02 10:41:31 +08:00
2e32de4716 ♻️ Migrated to nexus 2024-10-27 13:13:40 +08:00
07417deea9 🐛 Fix nil pointer panic 2024-10-20 14:12:36 +08:00
a9156992f4 🐛 Fix bug crash caused by hash small files 2024-10-18 23:36:54 +08:00
cbe034a049 🐛 Trying to prevent exiftool causing analyze failed 2024-10-18 23:13:27 +08:00
e123c82c2b Use exif whitelist to prevent produce garbage data 2024-10-16 22:12:35 +08:00
831717fabc Save EXIF into file metadata 2024-10-16 21:18:23 +08:00
6d57ce84e6 🔒 Fix Attachment will contains GPS information 2024-10-15 22:29:55 +08:00
fdadebbeab Able to get sticker pack by id 2024-09-25 22:55:15 +08:00
fa997a19a1 🐛 Fix getting count of stickers in pack 2024-09-25 22:28:08 +08:00
f7ab3bfb18 Optimize hash logic 2024-09-22 15:49:32 +08:00
c6311c466f Optimize merge files 2024-09-22 15:39:36 +08:00
db57e4764c ♻️ Refactored with new cache system 2024-09-22 13:45:02 +08:00
560c8a9143 Support broadcast deletion 2024-09-19 22:25:53 +08:00
e147937cbb ⬆️ Upgrade dealer 2024-09-17 16:47:12 +08:00
bf5c718780 Able to search sticker with alias 2024-09-16 20:44:40 +08:00
778d09d74a Lookup now will ignore case to find sticker 2024-09-16 20:40:57 +08:00
241d61758a 🐛 Fix didn't preload sticker pack in lookup 2024-09-16 20:35:59 +08:00
eabf0fdea1 🐛 Fix lookup sticker via alias didn't preload attachment 2024-09-16 20:22:30 +08:00
525a103a76 Be able to lookup sticker via thier pack prefix + alias 2024-09-15 18:39:09 +08:00
af1b45bd92 🐛 Fix userinfo insert into wrong table 2024-09-14 21:45:42 +08:00
98036b18b7 ♻️ User dealer's BaseModel 2024-09-11 23:55:46 +08:00
6c5a99e867 ♻️ Use the new dealer BaseUser and remove ExternalID 2024-09-11 23:49:20 +08:00
78705e82a8 Attachment API can edit metadata 2024-09-10 21:31:51 +08:00
42fedd7745 🐛 Fix account_id where clause causing indexing issue 2024-09-08 01:37:03 +08:00
94f4519164 ⬆️ Re-sum go mod 2024-08-23 19:37:20 +08:00
e89ef8548d ⬆️ Upgrade dealer package 2024-08-23 19:37:04 +08:00
644af9e7b2 🐛 Fix concurrent upload multipart cause incomplete 2024-08-22 01:30:14 +08:00
10eb0ba3bc 🐛 Fix uploader still using old cache api 2024-08-21 21:32:06 +08:00
fea0664d62 🐛 Fix direct upload will save as non-uploaded 2024-08-21 13:09:33 +08:00
1e4d7985ea 🐛 Fix analyzer didn't cache attachment 2024-08-21 10:51:11 +08:00
479b1933b0 🐛 Fix merged file isn't add to cache 2024-08-21 01:55:40 +08:00
cb02b66fca 🔀 Merge pull request ' 支援分片上传' (#3) from feature/multipart-upload into master
Reviewed-on: Hydrogen/Paperclip#3
2024-08-20 14:57:06 +00:00
7a8fa116d3 🐛 Bug fixes and optimization 2024-08-20 22:55:58 +08:00
37c47f9839 Make it more implementable 2024-08-20 19:17:43 +08:00
73b39a4fb5 Multipart file upload 2024-08-20 17:08:40 +08:00
e111e05033 Un-public indexable & select by pools 2024-08-19 01:49:27 +08:00
02f6ad9020 🐛 Fix crash on maintain cache 2024-08-18 22:45:35 +08:00
d2ff4087e5 💥 Use attachment rid instead of primary key when create 2024-08-18 22:12:06 +08:00
165a6be985 💥 Replace attachment id by rid when fetching 2024-08-18 21:10:10 +08:00
e92e5bad60 Support use rid to get file 2024-08-18 19:53:50 +08:00
0fb8e27e7c 🐛 Fix mark clean required issue 2024-08-18 17:20:05 +08:00
e7f6637398 Fix the RandString method cause the lag 2024-08-18 17:16:14 +08:00
f32803acf4 🐛 Fix doesn't get has lifecycle settings buckets correctly 2024-08-18 16:51:44 +08:00
6969a0e76c 🐛 Fix sql statement 2024-08-18 16:25:07 +08:00
1cce7c9b81 🐛 Fix schedule deletion will delete referenced file 2024-08-18 16:16:37 +08:00
a65b761d5e 🐛 Fix migration issue on pools 2024-08-18 16:02:25 +08:00
98cf753f66 ♻️ Split mark and delete file 2024-08-18 15:58:07 +08:00
922a76ee7f Pool clean by lifecycle config 2024-08-18 14:50:12 +08:00
a82fb3a49c Attachment has pool 2024-08-18 14:09:52 +08:00
dd0f7399a6 Attachment pool basis 2024-08-18 12:01:41 +08:00
de6215cffe List attachment original filter 2024-08-11 16:21:16 +08:00
d34e6f068b Self reference detection 2024-08-11 01:08:08 +08:00
3e000b6f9e Optimize list attachment api 2024-08-07 01:02:03 +08:00
13893e87f7 Cache in batch metadata api 2024-08-06 23:45:58 +08:00
4c5d6d5ec5 👔 Sticker creation & edit will no longer check metadta 2024-08-06 15:41:41 +08:00
8b3a7d6324 🐛 Fix manifest won't filter by author 2024-08-06 15:29:38 +08:00
dc57261b2e 🍺 Forgot apply the same code as create sticker is edit sticker 2024-08-04 00:10:26 +08:00
c381b53a29 🐛 Just forgot to add pagination data into response 2024-08-03 22:45:50 +08:00
75ab320bf1 Sticker will preload attachment 2024-08-03 22:04:44 +08:00
dfde530ca4 ⚗️ Try to fix metadata equals type issue 2024-08-03 21:53:14 +08:00
7e3e168d3c 🐛 Do not specfific metadata type to prevent causing the determine issue 2024-08-03 21:35:00 +08:00
1a8c28d2f9 🐛 Fix metadata attr type transform issue 2024-08-03 21:29:22 +08:00
7452b0754d 🗃️ Fix stickers migration issue 2024-08-03 16:30:01 +08:00
7af44da03b More stickers apis 2024-08-03 16:16:14 +08:00
ad1d82a2ff Stickers and sticker packs 2024-08-03 15:43:15 +08:00
8070a87078 🐛 Fix attachment meta dir to prevent further bugs 2024-08-03 03:22:45 +08:00
b28735bdd7 ⚗️ Can analyze video now 2024-08-02 22:31:30 +08:00
b093e280fe Batch list attachment meta 2024-08-02 21:52:32 +08:00
a696b13837 🐛 Fix metadata missing author info 2024-08-02 21:38:47 +08:00
03f08f1ff6 🐛 Fix get cannot get other attachment userinfo 2024-07-31 15:11:15 +08:00
8196aac3de Change update attachment policy to prevent conflict with analyzer 2024-07-29 21:11:42 +08:00
36c814e1dc 🔀 Merge pull request ' 使用服务器来计算元数据' (#2) from features/calc-in-backend into master
Reviewed-on: Hydrogen/Paperclip#2
2024-07-29 06:47:21 +00:00
84c3b16735 Background scan for unanalyzed files 2024-07-29 14:41:28 +08:00
cac19b2c93 🧵 Replace cache with thread-safe sync.Map 2024-07-29 13:31:48 +08:00
c46d7fa312 🐛 Bug fixes and logging 2024-07-29 13:22:57 +08:00
8f08d85fb1 🚀 Fix everything and ready to launch! 2024-07-29 01:47:33 +08:00
82cb45ec53 File operations queue 2024-07-29 00:53:40 +08:00
089a9ecd9d Processing files in background 2024-07-29 00:01:51 +08:00
020e59234e Reupload file to permanent storage 2024-07-28 23:16:46 +08:00
10879bef14 Basic queue for processing 2024-07-28 21:03:56 +08:00
2a94bb20f8 📝 Update README for the future features 2024-07-28 14:04:18 +08:00
763977c8f0 🗑️ Remove independent mode 2024-07-28 01:15:52 +08:00
36d13f4b81 Listing attachment will preload account 2024-07-27 00:23:22 +08:00
f5732726ed 🐛 Fix querying attachments by usage 2024-07-26 22:46:33 +08:00
168c089456 🐛 Fix fixes on listing attachments and improve performance 2024-07-26 16:42:16 +08:00
f91143ce9a 🐛 Fix filter by author requires internal id which is ridiculous 2024-07-26 16:32:01 +08:00
6055ca4274 🐛 Fix metadata wont preload author 2024-07-26 01:29:16 +08:00
0f1c787137 🐛 Fix where statement type error 2024-07-25 22:15:06 +08:00
01e395b2bf List attachments api 2024-07-25 22:12:00 +08:00
9ceb9925dd Independent mode 2024-07-25 22:02:26 +08:00
acd610464a Rollback api has no prefix 2024-07-16 18:08:23 +08:00
83df95435c 🚚 Move api path 2024-07-16 17:02:16 +08:00
69 changed files with 5222 additions and 1704 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
/uploads
/dist
/keys
.DS_Store
.idea

9
.idea/Paperclip.iml generated
View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Paperclip.iml" filepath="$PROJECT_DIR$/.idea/Paperclip.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

133
.idea/workspace.xml generated
View File

@@ -1,133 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="ALL" />
</component>
<component name="ChangeListManager">
<list default="true" id="18dd0d68-b4b8-40db-9734-9119b5c848bd" name="更改" comment=":recycle: Moved onto dealer">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/go.mod" beforeDir="false" afterPath="$PROJECT_DIR$/go.mod" afterDir="false" />
<change beforePath="$PROJECT_DIR$/go.sum" beforeDir="false" afterPath="$PROJECT_DIR$/go.sum" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/gap/client.go" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/gap/server.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/gap/server.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/server/exts/auth.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/server/exts/auth.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/services/auth.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/internal/services/auth.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/internal/services/jwt.go" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/pkg/main.go" beforeDir="false" afterPath="$PROJECT_DIR$/pkg/main.go" afterDir="false" />
<change beforePath="$PROJECT_DIR$/settings.toml" beforeDir="false" afterPath="$PROJECT_DIR$/settings.toml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="FileTemplateManagerImpl">
<option name="RECENT_TEMPLATES">
<list>
<option value="Go File" />
</list>
</option>
</component>
<component name="GOROOT" url="file:///opt/homebrew/opt/go/libexec" />
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="ProjectErrors" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 7
}</component>
<component name="ProjectId" id="2gauyxHu1OWsYigauXZCcaIhfso" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"DefaultGoTemplateProperty": "Go File",
"Go Build.Backend.executor": "Run",
"Go 构建.Backend.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.go.formatter.settings.were.checked": "true",
"RunOnceActivity.go.migrated.go.modules.settings": "true",
"RunOnceActivity.go.modules.automatic.dependencies.download": "true",
"RunOnceActivity.go.modules.go.list.on.any.changes.was.set": "true",
"git-widget-placeholder": "refactor/dealer",
"go.import.settings.migrated": "true",
"go.sdk.automatically.set": "true",
"last_opened_file_path": "/Users/littlesheep/Documents/Projects/Hydrogen/Paperclip/pkg/internal/grpc",
"node.js.detected.package.eslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"run.code.analysis.last.selected.profile": "pProject Default",
"settings.editor.selected.configurable": "preferences.lookFeel",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/pkg/internal/grpc" />
<recent name="$PROJECT_DIR$/pkg/internal/gap" />
<recent name="$PROJECT_DIR$/pkg/internal" />
</key>
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/pkg/internal/server/exts" />
<recent name="$PROJECT_DIR$/pkg/internal/server/api" />
<recent name="$PROJECT_DIR$/pkg" />
<recent name="$PROJECT_DIR$/pkg/internal" />
</key>
</component>
<component name="RunManager">
<configuration name="Backend" type="GoApplicationRunConfiguration" factoryName="Go Application">
<module name="Paperclip" />
<working_directory value="$PROJECT_DIR$" />
<kind value="FILE" />
<package value="git.solsynth.dev/hydrogen/paperclip" />
<directory value="$PROJECT_DIR$" />
<filePath value="$PROJECT_DIR$/pkg/main.go" />
<output_directory value="$PROJECT_DIR$/dist" />
<method v="2" />
</configuration>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-gosdk-33c477a475b1-e0158606a674-org.jetbrains.plugins.go.sharedIndexes.bundled-GO-241.18034.61" />
<option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-GO-241.18034.61" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="应用程序级" UseSingleDictionary="true" transferred="true" />
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="Vcs.Log.Tabs.Properties">
<option name="TAB_STATES">
<map>
<entry key="MAIN">
<value>
<State />
</value>
</entry>
</map>
</option>
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value=":sparkles: Upload attachment requires permission check" />
<MESSAGE value=":sparkles: Provide a faster check attachment exists grpc method" />
<MESSAGE value=":truck: Update url mapping" />
<MESSAGE value=":bug: Fix uuid duplicate when link exists" />
<MESSAGE value="&#10;:sparkles: Add health check" />
<MESSAGE value=":arrow_up: Upgrade Passport and use Hyper SDK" />
<MESSAGE value=":arrow_up: Upgrade Passport to fix bug" />
<MESSAGE value=":ambulance: Fix getting user panic" />
<MESSAGE value=":zap: Add cache into metadata fetching" />
<MESSAGE value=":recycle: Moved onto dealer" />
<option name="LAST_COMMIT_MESSAGE" value=":recycle: Moved onto dealer" />
</component>
<component name="VgoProject">
<settings-migrated>true</settings-migrated>
</component>
</project>

View File

@@ -10,6 +10,9 @@ FROM golang:alpine
COPY --from=paperclip-server /dist /paperclip/server
RUN apk add ffmpeg
RUN apk add exiftool
EXPOSE 8445
CMD ["/paperclip/server"]

View File

@@ -1,11 +1,24 @@
# Hydrogen.Paperclip
# Hypernet.Paperclip
Paperclip is the unified attachment service for all hydrogen services.
Paperclip is the unified attachment service for all hypernet services.
It contains file metadata compute, instant upload, calculating hashing, multi destination, media info and more features!
## Features
Paperclip store and processing uploaded files with pipeline flow.
When a user try to upload files. The file will store in local first for media processing.
Then the server will publish a message into the message queue.
And the background consumer will start dealing with the uploaded files.
The background consumer will hash the file and merge the files with same hashcode.
The background consumer will decode the image and generate ratio and read more info from image file too.
After the processing done. The consumer will upload the file to the permanent storage like a s3 bucket and remove local cache.
While the processing, the file record in database will marked to the temporary and load file from the temporary storage.
When the processing done, the file record will be updated.
### Supported Destinations
- Local filesystem
- S3 compilable bucket
- S3 compilable bucket

122
go.mod
View File

@@ -1,92 +1,104 @@
module git.solsynth.dev/hydrogen/paperclip
module git.solsynth.dev/hypernet/paperclip
go 1.21.6
go 1.23.2
require (
git.solsynth.dev/hydrogen/dealer v0.0.0-20240714155615-ad83100677ab
github.com/go-playground/validator/v10 v10.17.0
github.com/gofiber/fiber/v2 v2.52.4
git.solsynth.dev/hypernet/nexus v0.0.0-20250329075932-d5422ab5b04c
git.solsynth.dev/hypernet/passport v0.0.0-20250315083747-32e91e26013c
git.solsynth.dev/hypernet/wallet v0.0.0-20250323095812-468cd655f886
github.com/barasher/go-exiftool v1.10.0
github.com/dgraph-io/ristretto v0.2.0
github.com/eko/gocache/lib/v4 v4.2.0
github.com/eko/gocache/store/ristretto/v4 v4.2.2
github.com/fatih/color v1.18.0
github.com/go-playground/validator/v10 v10.22.1
github.com/gofiber/fiber/v2 v2.52.6
github.com/google/uuid v1.6.0
github.com/json-iterator/go v1.1.12
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607
github.com/minio/minio-go/v7 v7.0.70
github.com/robfig/cron/v3 v3.0.1
github.com/rs/zerolog v1.31.0
github.com/samber/lo v1.39.0
github.com/spf13/viper v1.18.2
google.golang.org/grpc v1.64.0
google.golang.org/protobuf v1.34.2
gorm.io/datatypes v1.2.0
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.6
github.com/rs/zerolog v1.33.0
github.com/samber/lo v1.47.0
github.com/schollz/progressbar/v3 v3.14.4
github.com/spf13/cast v1.7.0
github.com/spf13/viper v1.19.0
golang.org/x/image v0.0.0-20190802002840-cff245a6509b
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.4
gopkg.in/vansante/go-ffprobe.v2 v2.2.0
gorm.io/datatypes v1.2.4
gorm.io/driver/postgres v1.5.9
gorm.io/gorm v1.25.12
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/armon/go-metrics v0.4.1 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-playground/form v3.1.4+incompatible // indirect
github.com/eko/gocache/store/redis/v4 v4.2.2 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/hashicorp/consul/api v1.29.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
github.com/golang/mock v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
github.com/jackc/pgx/v5 v5.5.1 // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.1 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/klauspost/compress v1.17.8 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.2.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mbobakov/grpc-consul-resolver v1.5.3 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.19.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.52.3 // indirect
github.com/prometheus/procfs v0.13.0 // indirect
github.com/redis/go-redis/v9 v9.7.3 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.1.8 // indirect
github.com/tinylib/msgp v1.2.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.52.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
github.com/valyala/fasthttp v1.59.0 // indirect
go.uber.org/mock v0.4.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect
golang.org/x/net v0.26.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect
golang.org/x/crypto v0.33.0 // indirect
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sync v0.11.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/term v0.29.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/mysql v1.5.2 // indirect
gorm.io/driver/mysql v1.5.7 // indirect
)

458
go.sum
View File

@@ -1,412 +1,296 @@
git.solsynth.dev/hydrogen/dealer v0.0.0-20240714155615-ad83100677ab h1:hxqJkL4Ha3Y3SfeSA9TPKBJK6mtY88Q2uialHsDbGf0=
git.solsynth.dev/hydrogen/dealer v0.0.0-20240714155615-ad83100677ab/go.mod h1:eZwAwP7ahL7TO8GWBlYFYDdjlna+8zHYbDfNabnuUEU=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329072729-4a08fd8f1c46 h1:oH2jq7ZG5cslCULUMWqv4dS/YNvd+Xcuv4rBPj0uGA8=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329072729-4a08fd8f1c46/go.mod h1:5tk62VQ1DcbR0EAN2jAOqYxHiegUPEC805JlfQ/G19I=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329075932-d5422ab5b04c h1:XgdTgJxSAQuCbiG15hN5pY6chzcz8sX3Onm2itS+Ufs=
git.solsynth.dev/hypernet/nexus v0.0.0-20250329075932-d5422ab5b04c/go.mod h1:5tk62VQ1DcbR0EAN2jAOqYxHiegUPEC805JlfQ/G19I=
git.solsynth.dev/hypernet/passport v0.0.0-20250315083747-32e91e26013c h1:XB8EBX34WB2skmjaVFot5IlxKF2qFZ2SueG/Y9SiJ6Y=
git.solsynth.dev/hypernet/passport v0.0.0-20250315083747-32e91e26013c/go.mod h1:k7MZQWYBpxlk3g9bx0HTh5C3m+MG/wr0hAiRM/VyAqs=
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88 h1:2HEENe9KUrdaJeNBzx9lsuXQGyzWqCgnLTKQnr8xFr8=
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88/go.mod h1:ildzMtLagNsLK0Rkw4Hgk2TrrwqZnjwJIUx0MNZwcDY=
git.solsynth.dev/hypernet/wallet v0.0.0-20250323095812-468cd655f886 h1:rVssXF8jZ64ctAfzlCgIgF22NCT9VAPAVxrwlcItx3s=
git.solsynth.dev/hypernet/wallet v0.0.0-20250323095812-468cd655f886/go.mod h1:rmomNGQ6RBSp8TpZGA8tFr5M54AL2NADJ/1n0MfrIRM=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/barasher/go-exiftool v1.10.0 h1:f5JY5jc42M7tzR6tbL9508S2IXdIcG9QyieEXNMpIhs=
github.com/barasher/go-exiftool v1.10.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgraph-io/ristretto v0.2.0 h1:XAfl+7cmoUDWW/2Lx8TGZQjjxIQ2Ley9DSf52dru4WE=
github.com/dgraph-io/ristretto v0.2.0/go.mod h1:8uBHCU/PBV4Ag0CJrP47b9Ofby5dqWNh4FicAdoqFNU=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
github.com/eko/gocache/store/redis/v4 v4.2.2 h1:Thw31fzGuH3WzJywsdbMivOmP550D6JS7GDHhvCJPA0=
github.com/eko/gocache/store/redis/v4 v4.2.2/go.mod h1:LaTxLKx9TG/YUEybQvPMij++D7PBTIJ4+pzvk0ykz0w=
github.com/eko/gocache/store/ristretto/v4 v4.2.2 h1:lXFzoZ5ck6Gy6ON7f5DHSkNt122qN7KoroCVgVwF7oo=
github.com/eko/gocache/store/ristretto/v4 v4.2.2/go.mod h1:uIvBVJzqRepr5L0RsbkfQ2iYfbyos2fuji/s4yM+aUM=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/form v3.1.4+incompatible h1:lvKiHVxE2WvzDIoyMnWcjyiBxKt2+uFJyZcPYWsLnjI=
github.com/go-playground/form v3.1.4+incompatible/go.mod h1:lhcKXfTuhRtIZCIKUeJ0b5F207aeQCPbZU09ScKjwWg=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/consul/api v1.29.1 h1:UEwOjYJrd3lG1x5w7HxDRMGiAUPrb3f103EoeKuuEcc=
github.com/hashicorp/consul/api v1.29.1/go.mod h1:lumfRkY/coLuqMICkI7Fh3ylMG31mQSRZyef2c5YvJI=
github.com/hashicorp/consul/proto-public v0.6.1 h1:+uzH3olCrksXYWAYHKqK782CtK9scfqH+Unlw3UHhCg=
github.com/hashicorp/consul/proto-public v0.6.1/go.mod h1:cXXbOg74KBNGajC+o8RlA502Esf0R9prcoJgiOX/2Tg=
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
github.com/hashicorp/consul/sdk v0.16.1/go.mod h1:fSXvwxB2hmh1FMZCNl6PwX0Q/1wdWtHJcZ7Ea5tns0s=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=
github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR/prTM=
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg=
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607 h1:8tP9cdXzcGX2AvweVVG/lxbI7BSjWbNNUustwJ9dQVA=
github.com/kettek/apng v0.0.0-20220823221153-ff692776a607/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mbobakov/grpc-consul-resolver v1.5.3 h1:xL7nJm8qCvxgHMqlnF4naXruBUoHqfUWORl3UmwKByU=
github.com/mbobakov/grpc-consul-resolver v1.5.3/go.mod h1:0wN8+McBocuk5mO9xlAfrmBSothm7sps43bFGubg0m4=
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
github.com/microsoft/go-mssqldb v0.17.0/go.mod h1:OkoNGhGEs8EZqchVTtochlXruEhEOaO4S0d2sB5aeGQ=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJnn2g=
github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo=
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74=
github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0=
google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY=
google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/vansante/go-ffprobe.v2 v2.2.0 h1:iuOqTsbfYuqIz4tAU9NWh22CmBGxlGHdgj4iqP+NUmY=
gopkg.in/vansante/go-ffprobe.v2 v2.2.0/go.mod h1:qF0AlAjk7Nqzqf3y333Ly+KxN3cKF2JqA3JT5ZheUGE=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco=
gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04=
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/datatypes v1.2.4 h1:uZmGAcK/QZ0uyfCuVg0VQY1ZmV9h1fuG0tMwKByO1z4=
gorm.io/datatypes v1.2.4/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=

132
pkg/filekit/io.go Normal file
View File

@@ -0,0 +1,132 @@
package filekit
import (
"context"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/proto"
"github.com/goccy/go-json"
)
func GetAttachment(c *nex.Conn, rid string) (models.Attachment, error) {
cacheConn, err := cachekit.NewConn(c, 3*time.Second)
if err == nil {
key := cachekit.FKey(cachekit.DAAttachment, rid)
if attachment, err := cachekit.Get[models.Attachment](cacheConn, key); err == nil {
return attachment, nil
}
}
var attachment models.Attachment
conn, err := c.GetClientGrpcConn("uc")
if err != nil {
return attachment, nil
}
pc := proto.NewAttachmentServiceClient(conn)
resp, err := pc.GetAttachment(context.Background(), &proto.GetAttachmentRequest{
Rid: &rid,
})
if err != nil {
return attachment, err
}
if err := json.Unmarshal(resp.Attachment, &attachment); err != nil {
return attachment, err
}
return attachment, nil
}
func ListAttachment(c *nex.Conn, rid []string) ([]models.Attachment, error) {
var attachments []models.Attachment
var missingRid []string
cachedAttachments := make(map[string]models.Attachment)
// Try to get attachments from cache
cacheConn, err := cachekit.NewConn(c, 3*time.Second)
if err == nil {
for _, rid := range rid {
key := cachekit.FKey(cachekit.DAAttachment, rid)
if attachment, err := cachekit.Get[models.Attachment](cacheConn, key); err == nil {
cachedAttachments[rid] = attachment
} else {
missingRid = append(missingRid, rid)
}
}
}
// If all attachments are found in cache, return them
if len(missingRid) == 0 {
for _, attachment := range cachedAttachments {
attachments = append(attachments, attachment)
}
return attachments, nil
}
// Fetch missing attachments from the gRPC service
conn, err := c.GetClientGrpcConn("uc")
if err != nil {
return attachments, err
}
pc := proto.NewAttachmentServiceClient(conn)
resp, err := pc.ListAttachment(context.Background(), &proto.ListAttachmentRequest{
Rid: missingRid,
})
if err != nil {
return attachments, err
}
// Parse the fetched attachments
for _, item := range resp.GetAttachments() {
var attachment models.Attachment
if err := json.Unmarshal(item, &attachment); err != nil {
return attachments, err
}
attachments = append(attachments, attachment)
}
// Merge cached and fetched results
for _, attachment := range cachedAttachments {
attachments = append(attachments, attachment)
}
return attachments, nil
}
func UpdateVisibility(c *nex.Conn, request *proto.UpdateVisibilityRequest) error {
conn, err := c.GetClientGrpcConn("uc")
if err != nil {
return nil
}
pc := proto.NewAttachmentServiceClient(conn)
_, err = pc.UpdateVisibility(context.Background(), request)
return err
}
func DeleteAttachment(c *nex.Conn, request *proto.DeleteAttachmentRequest) error {
conn, err := c.GetClientGrpcConn("uc")
if err != nil {
return nil
}
pc := proto.NewAttachmentServiceClient(conn)
_, err = pc.DeleteAttachment(context.Background(), request)
return err
}
func CountAttachmentUsage(c *nex.Conn, request *proto.UpdateUsageRequest) error {
conn, err := c.GetClientGrpcConn("uc")
if err != nil {
return nil
}
pc := proto.NewAttachmentServiceClient(conn)
_, err = pc.UpdateUsage(context.Background(), request)
return err
}

View File

@@ -0,0 +1,142 @@
package models
import (
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
"gorm.io/datatypes"
"gorm.io/gorm"
)
const (
AttachmentDstExternal = -1 // The destination marked the file did not store inside our service
AttachmentDstTemporary = 0 // Destination 0 is a reserved config for pre-upload processing
)
const (
AttachmentTypeNormal = iota
AttachmentTypeThumbnail
AttachmentTypeCompressed
)
type Attachment struct {
cruda.BaseModel
// Random ID is for accessing (appear in URL)
Rid string `json:"rid" gorm:"uniqueIndex"`
// Unique ID is for storing (appear in local file name or object name)
Uuid string `json:"uuid"`
Size int64 `json:"size"`
Name string `json:"name"`
Alternative string `json:"alt"`
MimeType string `json:"mimetype"`
HashCode string `json:"hash"`
Destination int `json:"destination"`
RefCount int `json:"ref_count"`
Type uint `json:"type"`
CleanedAt *time.Time `json:"cleaned_at"`
Metadata datatypes.JSONMap `json:"metadata"` // This field is analyzer auto generated metadata
Usermeta datatypes.JSONMap `json:"usermeta"` // This field is user set metadata
ContentRating int `json:"content_rating"` // This field use to filter mature content or not
QualityRating int `json:"quality_rating"` // This field use to filter good content or not
IsAnalyzed bool `json:"is_analyzed"`
IsSelfRef bool `json:"is_self_ref"`
IsIndexable bool `json:"is_indexable"` // Show this attachment in the public directory api or not
// Count the usage of this attachment across all services
// If this number remain 0, it will be deleted during the maintenance
UsedCount int `json:"used_count"`
Thumbnail *Attachment `json:"thumbnail"`
ThumbnailID *uint `json:"thumbnail_id"`
Compressed *Attachment `json:"compressed"`
CompressedID *uint `json:"compressed_id"`
Ref *Attachment `json:"ref"`
RefID *uint `json:"ref_id"`
RefURL *string `json:"ref_url"` // External URL for the attachment
Pool *AttachmentPool `json:"pool"`
PoolID *uint `json:"pool_id"`
Boosts []AttachmentBoost `json:"boosts"`
AccountID uint `json:"account_id"`
Account models.Account `gorm:"-" json:"account"`
}
func (v *Attachment) AfterUpdate(tx *gorm.DB) error {
_ = cachekit.Delete(
gap.Ca,
cachekit.FKey(cachekit.DAAttachment, v.Rid),
)
return nil
}
// Data model for in progress multipart attachments
type AttachmentFragment struct {
cruda.BaseModel
// Random ID is for accessing (appear in URL)
Rid string `json:"rid" gorm:"uniqueIndex"`
// Unique ID is for storing (appear in local file name or object name)
Uuid string `json:"uuid"`
Size int64 `json:"size"`
Name string `json:"name"`
Alternative string `json:"alt"`
MimeType string `json:"mimetype"`
HashCode string `json:"hash"`
Fingerprint *string `json:"fingerprint"` // Client side generated hash, for continue uploading
FileChunks datatypes.JSONMap `json:"file_chunks"`
Metadata datatypes.JSONMap `json:"metadata"` // This field is analyzer auto generated metadata
Usermeta datatypes.JSONMap `json:"usermeta"` // This field is user set metadata
Pool *AttachmentPool `json:"pool"`
PoolID *uint `json:"pool_id"`
AccountID uint `json:"account_id"`
FileChunksMissing []string `json:"file_chunks_missing" gorm:"-"` // This field use to prompt client which chunks is pending upload, do not store it
}
func (v *AttachmentFragment) AfterUpdate(tx *gorm.DB) error {
_ = cachekit.Delete(
gap.Ca,
cachekit.FKey("attachment-fragment", v.Rid),
)
return nil
}
func (v *AttachmentFragment) ToAttachment() Attachment {
return Attachment{
Rid: v.Rid,
Uuid: v.Uuid,
Size: v.Size,
Name: v.Name,
Alternative: v.Alternative,
MimeType: v.MimeType,
HashCode: v.HashCode,
Metadata: v.Metadata,
Usermeta: v.Usermeta,
Destination: AttachmentDstTemporary,
Type: AttachmentTypeNormal,
Pool: v.Pool,
PoolID: v.PoolID,
AccountID: v.AccountID,
}
}

View File

@@ -0,0 +1,24 @@
package models
import "git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
const (
BoostStatusPending = iota
BoostStatusActive
BoostStatusSuspended
BoostStatusError
)
// AttachmentBoost is made for speed up attachment loading by copy the original attachments
// to others faster CDN or storage destinations.
type AttachmentBoost struct {
cruda.BaseModel
Status int `json:"status"`
Destination int `json:"destination"`
AttachmentID uint `json:"attachment_id"`
Attachment Attachment `json:"attachment"`
AccountID uint `json:"account"`
}

View File

@@ -0,0 +1,54 @@
package models
import (
pkg "git.solsynth.dev/hypernet/paperclip/pkg/internal"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
const (
DestinationTypeLocal = "local"
DestinationTypeS3 = "s3"
)
type BaseDestination struct {
ID int `json:"id,omitempty"` // Auto filled with index, only for user
Type string `json:"type"`
Label string `json:"label"`
Region string `json:"region"`
IsBoost bool `json:"is_boost"`
}
type LocalDestination struct {
BaseDestination
Path string `json:"path"`
AccessBaseURL string `json:"access_baseurl"`
}
type S3Destination struct {
BaseDestination
Path string `json:"path"`
Bucket string `json:"bucket"`
Endpoint string `json:"endpoint"`
SecretID string `json:"secret_id"`
SecretKey string `json:"secret_key"`
AccessBaseURL string `json:"access_baseurl"`
ImageProxyURL string `json:"image_proxy_baseurl"`
EnableSSL bool `json:"enable_ssl"`
EnableSigned bool `json:"enable_signed"`
BucketLookup int `json:"bucket_lookup"`
}
func (v S3Destination) GetClient() (*minio.Client, error) {
client, err := minio.New(v.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(v.SecretID, v.SecretKey, ""),
Secure: v.EnableSSL,
BucketLookup: minio.BucketLookupType(v.BucketLookup),
})
if err == nil {
client.SetAppInfo("HyperNet.Paperclip", pkg.AppVersion)
}
return client, err
}

View File

@@ -0,0 +1,27 @@
package models
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"gorm.io/datatypes"
)
type AttachmentPool struct {
cruda.BaseModel
Alias string `json:"alias"`
Name string `json:"name"`
Description string `json:"description"`
Config datatypes.JSONType[AttachmentPoolConfig] `json:"config"`
Attachments []Attachment `json:"attachments" gorm:"foreignKey:PoolID"`
AccountID *uint `json:"account_id"`
}
type AttachmentPoolConfig struct {
MaxFileSize *int64 `json:"max_file_size"`
ExistLifecycle *int64 `json:"exist_lifecycle"`
AllowCrossPoolIngress bool `json:"allow_cross_pool_ingress"`
AllowCrossPoolEgress bool `json:"allow_cross_pool_egress"`
PublicIndexable bool `json:"public_indexable"`
}

View File

@@ -0,0 +1,32 @@
package models
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
)
type Sticker struct {
cruda.BaseModel
Alias string `json:"alias"`
Name string `json:"name"`
AttachmentID uint `json:"attachment_id"`
Attachment Attachment `json:"attachment"`
PackID uint `json:"pack_id"`
Pack StickerPack `json:"pack"`
AccountID uint `json:"account_id"`
}
type StickerPack struct {
cruda.BaseModel
Prefix string `json:"prefix"`
Name string `json:"name"`
Description string `json:"description"`
Stickers []Sticker `json:"stickers" gorm:"foreignKey:PackID;constraint:OnDelete:CASCADE"`
AccountID uint `json:"account_id"`
}
type StickerPackOwnership struct {
PackID uint `json:"pack_id" gorm:"primaryKey"`
AccountID uint `json:"account_id" gorm:"primaryKey"`
}

View File

@@ -1,13 +1,18 @@
package database
import (
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/models"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"gorm.io/gorm"
)
var AutoMaintainRange = []any{
&models.Account{},
&models.AttachmentPool{},
&models.Attachment{},
&models.AttachmentFragment{},
&models.AttachmentBoost{},
&models.StickerPack{},
&models.Sticker{},
&models.StickerPackOwnership{},
}
func RunMigration(source *gorm.DB) error {

View File

@@ -1,24 +1,23 @@
package database
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
var C *gorm.DB
func NewSource() error {
func NewGorm() error {
var err error
dialector := postgres.Open(viper.GetString("database.dsn"))
C, err = gorm.Open(dialector, &gorm.Config{NamingStrategy: schema.NamingStrategy{
TablePrefix: viper.GetString("database.prefix"),
}, Logger: logger.New(&log.Logger, logger.Config{
dsn, err := cruda.NewCrudaConn(gap.Nx).AllocDatabase("paperclip")
C, err = gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.New(&log.Logger, logger.Config{
Colorful: true,
IgnoreRecordNotFoundError: true,
LogLevel: lo.Ternary(viper.GetBool("debug.database"), logger.Info, logger.Silent),

View File

@@ -0,0 +1,3 @@
# File System
The reason of why this package exists is because "cycle import was not allowed"

View File

@@ -0,0 +1,50 @@
package fs
import (
"context"
"fmt"
"os"
"path/filepath"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
pkg "git.solsynth.dev/hypernet/paperclip/pkg/internal"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio-go/v7"
"github.com/spf13/viper"
)
func DownloadFileToLocal(meta models.Attachment, dst int) (string, error) {
destMap := viper.GetStringMap(fmt.Sprintf("destinations.%d", dst))
var dest models.BaseDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
switch dest.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return filepath.Join(destConfigured.Path, meta.Uuid), nil
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
client, err := destConfigured.GetClient()
client.SetAppInfo("HyperNet.Paperclip", pkg.AppVersion)
if err != nil {
return "", fmt.Errorf("unable to configure s3 client: %v", err)
}
inDst := filepath.Join(os.TempDir(), meta.Uuid)
err = client.FGetObject(context.Background(), destConfigured.Bucket, meta.Uuid, inDst, minio.GetObjectOptions{})
if err != nil {
return "", fmt.Errorf("unable to upload file to s3: %v", err)
}
return inDst, nil
default:
return "", fmt.Errorf("invalid destination: unsupported protocol %s", dest.Type)
}
}

72
pkg/internal/fs/merger.go Normal file
View File

@@ -0,0 +1,72 @@
package fs
import (
"fmt"
"io"
"os"
"path/filepath"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"github.com/spf13/viper"
)
func MergeFileChunks(meta models.AttachmentFragment, arrange []string) (models.Attachment, error) {
attachment := meta.ToAttachment()
// Fetch destination from config
destMap := viper.GetStringMapString("destinations.0")
var dest models.LocalDestination
dest.Path = destMap["path"]
// Create the destination file
destPath := filepath.Join(dest.Path, meta.Uuid)
destFile, err := os.Create(destPath)
if err != nil {
return attachment, err
}
defer destFile.Close()
// 32KB buffer
buf := make([]byte, 32*1024)
// Merge the chunks into the destination file
for _, chunk := range arrange {
chunkPath := filepath.Join(dest.Path, fmt.Sprintf("%s.part%s", meta.Uuid, chunk))
chunkFile, err := os.Open(chunkPath)
if err != nil {
return attachment, err
}
defer chunkFile.Close() // Ensure the file is closed after reading
for {
n, err := chunkFile.Read(buf)
if err != nil && err != io.EOF {
return attachment, err
}
if n == 0 {
break
}
if _, err := destFile.Write(buf[:n]); err != nil {
return attachment, err
}
}
}
// Clean up: remove chunk files
go DeleteFragment(meta)
for _, chunk := range arrange {
chunkPath := filepath.Join(dest.Path, fmt.Sprintf("%s.part%s", meta.Uuid, chunk))
if err := os.Remove(chunkPath); err != nil {
return attachment, err
}
}
// Clean up: remove fragment record
database.C.Delete(&meta)
return attachment, nil
}

133
pkg/internal/fs/recycler.go Normal file
View File

@@ -0,0 +1,133 @@
package fs
import (
"context"
"fmt"
"os"
"path/filepath"
"time"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"github.com/samber/lo"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio-go/v7"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
)
func RunMarkLifecycleDeletionTask() {
var pools []models.AttachmentPool
if err := database.C.Find(&pools).Error; err != nil {
return
}
var pendingPools []models.AttachmentPool
for _, pool := range pools {
if pool.Config.Data().ExistLifecycle != nil {
pendingPools = append(pendingPools, pool)
}
}
for _, pool := range pendingPools {
lifecycle := time.Now().Add(-time.Duration(*pool.Config.Data().ExistLifecycle) * time.Second)
tx := database.C.
Where("pool_id = ?", pool.ID).
Where("created_at < ?", lifecycle).
Where("cleaned_at IS NULL").
Updates(&models.Attachment{CleanedAt: lo.ToPtr(time.Now())})
log.Info().
Str("pool", pool.Alias).
Int64("count", tx.RowsAffected).
Err(tx.Error).
Msg("Marking attachments as clean needed due to pool's lifecycle configuration...")
}
}
func RunMarkMultipartDeletionTask() {
lifecycle := time.Now().Add(-60 * time.Minute)
tx := database.C.
Where("created_at < ?", lifecycle).
Where("is_uploaded = ?", false).
Where("cleaned_at IS NULL").
Updates(&models.Attachment{CleanedAt: lo.ToPtr(time.Now())})
log.Info().
Int64("count", tx.RowsAffected).
Err(tx.Error).
Msg("Marking attachments as clean needed due to multipart lifecycle...")
}
func RunScheduleDeletionTask() {
var attachments []models.Attachment
if err := database.C.Where("cleaned_at IS NOT NULL").Find(&attachments).Error; err != nil {
return
}
for _, attachment := range attachments {
if attachment.RefID != nil {
continue
}
if err := DeleteFile(attachment); err != nil {
log.Error().
Uint("id", attachment.ID).
Msg("An error occurred when deleting marked clean up attachments...")
}
}
database.C.Where("cleaned_at IS NOT NULL").Delete(&models.Attachment{})
}
func DeleteFragment(meta models.AttachmentFragment) error {
destMap := viper.GetStringMap("destinations.0")
var dest models.LocalDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
for cid := range meta.FileChunks {
path := filepath.Join(dest.Path, fmt.Sprintf("%s.part%s", meta.Uuid, cid))
_ = os.Remove(path)
}
return nil
}
func DeleteFile(meta models.Attachment) error {
destMap := viper.GetStringMap(fmt.Sprintf("destinations.%d", meta.Destination))
var dest models.BaseDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
switch dest.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return DeleteFileFromLocal(destConfigured, meta.Uuid)
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return DeleteFileFromS3(destConfigured, meta.Uuid)
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", dest.Type)
}
}
func DeleteFileFromLocal(config models.LocalDestination, uuid string) error {
fullpath := filepath.Join(config.Path, uuid)
return os.Remove(fullpath)
}
func DeleteFileFromS3(config models.S3Destination, uuid string) error {
client, err := config.GetClient()
if err != nil {
return fmt.Errorf("unable to configure s3 client: %v", err)
}
err = client.RemoveObject(context.Background(), config.Bucket, filepath.Join(config.Path, uuid), minio.RemoveObjectOptions{})
if err != nil {
return fmt.Errorf("unable to upload file to s3: %v", err)
}
return nil
}

View File

@@ -1,15 +0,0 @@
package gap
import "net"
func GetOutboundIP() (net.IP, error) {
conn, err := net.Dial("udp", "1.1.1.1:80")
if err != nil {
return nil, err
} else {
defer conn.Close()
}
localAddr := conn.LocalAddr().(*net.UDPAddr)
return localAddr.IP, nil
}

View File

@@ -2,41 +2,52 @@ package gap
import (
"fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/hyper"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"github.com/rs/zerolog/log"
"strings"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
)
var H *hyper.HyperConn
var (
Nx *nex.Conn
Ca *cachekit.Conn
)
func RegisterService() error {
func InitializeToNexus() error {
grpcBind := strings.SplitN(viper.GetString("grpc_bind"), ":", 2)
httpBind := strings.SplitN(viper.GetString("bind"), ":", 2)
outboundIp, _ := GetOutboundIP()
outboundIp, _ := nex.GetOutboundIP()
grpcOutbound := fmt.Sprintf("%s:%s", outboundIp, grpcBind[1])
httpOutbound := fmt.Sprintf("%s:%s", outboundIp, httpBind[1])
var err error
H, err = hyper.NewHyperConn(viper.GetString("dealer.addr"), &proto.ServiceInfo{
Nx, err = nex.NewNexusConn(viper.GetString("nexus_addr"), &proto.ServiceInfo{
Id: viper.GetString("id"),
Type: hyper.ServiceTypeFileProvider,
Type: "uc",
Label: "Paperclip",
GrpcAddr: grpcOutbound,
HttpAddr: &httpOutbound,
HttpAddr: lo.ToPtr("http://" + httpOutbound + "/api"),
})
if err == nil {
go func() {
err := H.KeepRegisterService()
err := Nx.RunRegistering()
if err != nil {
log.Error().Err(err).Msg("An error occurred while registering service...")
}
}()
}
if Ca, err = cachekit.NewConn(Nx, 3*time.Second); err != nil {
return fmt.Errorf("failed to create cachekit connection: %v", err)
}
return err
}

View File

@@ -0,0 +1,153 @@
package grpc
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"git.solsynth.dev/hypernet/paperclip/pkg/proto"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (v *Server) GetAttachment(ctx context.Context, request *proto.GetAttachmentRequest) (*proto.GetAttachmentResponse, error) {
tx := database.C
if request.Id != nil {
tx = tx.Where("id = ?", request.Id)
} else if request.Rid != nil {
tx = tx.Where("rid = ?", request.Rid)
} else {
return nil, status.Error(codes.InvalidArgument, "you must provide id or random id")
}
if request.UserId != nil {
tx = tx.Where("account_id = ?", request.UserId)
}
var attachment models.Attachment
if err := tx.First(&attachment).Error; err != nil {
return nil, status.Error(codes.NotFound, "attachment not found")
}
out, err := services.CompleteAttachmentMeta(attachment)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &proto.GetAttachmentResponse{
Attachment: nex.EncodeMap(out[0]),
}, nil
}
func (v *Server) ListAttachment(ctx context.Context, request *proto.ListAttachmentRequest) (*proto.ListAttachmentResponse, error) {
tx := database.C
if len(request.Id) == 0 && len(request.Rid) == 0 {
return nil, status.Error(codes.InvalidArgument, "you must provide at least one id or random id")
}
if len(request.Id) > 0 {
tx = tx.Where("id IN ?", request.Id)
}
if len(request.Rid) > 0 {
tx = tx.Where("rid IN ?", request.Rid)
}
attachments := make([]models.Attachment, 0)
err := tx.Find(&attachments).Error
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
out, err := services.CompleteAttachmentMeta(attachments...)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
return &proto.ListAttachmentResponse{
Attachments: lo.Map(out, func(v models.Attachment, _ int) []byte {
return nex.EncodeMap(v)
}),
}, nil
}
func (v *Server) UpdateVisibility(ctx context.Context, request *proto.UpdateVisibilityRequest) (*proto.UpdateVisibilityResponse, error) {
log.Debug().Any("request", request).Msg("Update attachment visibility via grpc...")
tx := database.C
if len(request.Id) == 0 && len(request.Rid) == 0 {
return nil, status.Error(codes.InvalidArgument, "you must provide at least one id or random id")
}
if len(request.Id) > 0 {
tx = tx.Where("id IN ?", request.Id)
}
if len(request.Rid) > 0 {
tx = tx.Where("rid IN ?", request.Rid)
}
if request.UserId != nil {
tx = tx.Where("account_id = ?", request.UserId)
}
var rowsAffected int64
if err := tx.Updates(&models.Attachment{IsIndexable: request.IsIndexable}).Error; err != nil {
return nil, status.Error(codes.Internal, err.Error())
} else {
rowsAffected = tx.RowsAffected
}
return &proto.UpdateVisibilityResponse{
Count: int32(rowsAffected),
}, nil
}
func (v *Server) UpdateUsage(ctx context.Context, request *proto.UpdateUsageRequest) (*proto.UpdateUsageResponse, error) {
tx := database.C
if len(request.Id) == 0 && len(request.Rid) == 0 {
return nil, status.Error(codes.InvalidArgument, "you must provide at least one id or random id")
}
if len(request.Id) > 0 {
tx = tx.Where("id IN ?", request.Id)
}
if len(request.Rid) > 0 {
tx = tx.Where("rid IN ?", request.Rid)
}
if rows, err := services.CountAttachmentUsage(tx, int(request.GetDelta())); err != nil {
return nil, status.Error(codes.Internal, err.Error())
} else {
return &proto.UpdateUsageResponse{
Count: int32(rows),
}, nil
}
}
func (v *Server) DeleteAttachment(ctx context.Context, request *proto.DeleteAttachmentRequest) (*proto.DeleteAttachmentResponse, error) {
tx := database.C
if len(request.Id) == 0 && len(request.Rid) == 0 {
return nil, status.Error(codes.InvalidArgument, "you must provide at least one id or random id")
}
if len(request.Id) > 0 {
tx = tx.Where("id IN ?", request.Id)
}
if len(request.Rid) > 0 {
tx = tx.Where("rid IN ?", request.Rid)
}
if request.UserId != nil {
tx = tx.Where("account_id = ?", request.UserId)
}
var rowsAffected int64
if err := tx.Delete(&models.Attachment{}).Error; err != nil {
return nil, status.Error(codes.Internal, err.Error())
} else {
rowsAffected = tx.RowsAffected
}
return &proto.DeleteAttachmentResponse{
Count: int32(rowsAffected),
}, nil
}

View File

@@ -1,70 +0,0 @@
package grpc
import (
"context"
"fmt"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"git.solsynth.dev/hydrogen/paperclip/pkg/proto"
"google.golang.org/protobuf/types/known/emptypb"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/models"
jsoniter "github.com/json-iterator/go"
)
func (v *Server) GetAttachment(ctx context.Context, request *proto.AttachmentLookupRequest) (*proto.Attachment, error) {
var attachment models.Attachment
tx := database.C.Model(&models.Attachment{})
if request.Id != nil {
tx = tx.Where("id = ?", request.GetId())
}
if request.Uuid != nil {
tx = tx.Where("uuid = ?", request.GetUuid())
}
if request.Usage != nil {
tx = tx.Where("usage = ?", request.GetUsage())
}
if err := tx.First(&attachment).Error; err != nil {
return nil, err
}
rawMetadata, _ := jsoniter.Marshal(attachment.Metadata)
return &proto.Attachment{
Id: uint64(attachment.ID),
Uuid: attachment.Uuid,
Size: attachment.Size,
Name: attachment.Name,
Alt: attachment.Alternative,
Usage: attachment.Usage,
Mimetype: attachment.MimeType,
Hash: attachment.HashCode,
Destination: attachment.Destination,
Metadata: rawMetadata,
IsMature: attachment.IsMature,
AccountId: uint64(attachment.AccountID),
}, nil
}
func (v *Server) CheckAttachmentExists(ctx context.Context, request *proto.AttachmentLookupRequest) (*emptypb.Empty, error) {
tx := database.C.Model(&models.Attachment{})
if request.Id != nil {
tx = tx.Where("id = ?", request.GetId())
}
if request.Uuid != nil {
tx = tx.Where("uuid = ?", request.GetUuid())
}
if request.Usage != nil {
tx = tx.Where("usage = ?", request.GetUsage())
}
var count int64
if err := tx.Model(&models.Attachment{}).Count(&count).Error; err != nil {
return nil, err
} else if count == 0 {
return nil, fmt.Errorf("record not found")
}
return &emptypb.Empty{}, nil
}

View File

@@ -1,34 +1,43 @@
package grpc
import (
"git.solsynth.dev/hydrogen/paperclip/pkg/proto"
"git.solsynth.dev/hypernet/paperclip/pkg/proto"
"net"
nproto "git.solsynth.dev/hypernet/nexus/pkg/proto"
"github.com/spf13/viper"
"google.golang.org/grpc"
health "google.golang.org/grpc/health/grpc_health_v1"
"google.golang.org/grpc/reflection"
"net"
)
type Server struct {
proto.UnimplementedAttachmentsServer
nproto.UnimplementedDirectoryServiceServer
proto.UnimplementedAttachmentServiceServer
health.UnimplementedHealthServer
srv *grpc.Server
}
var S *grpc.Server
func NewGrpc() *Server {
server := &Server{
srv: grpc.NewServer(),
}
func NewGRPC() {
S = grpc.NewServer()
nproto.RegisterDirectoryServiceServer(server.srv, server)
proto.RegisterAttachmentServiceServer(server.srv, server)
health.RegisterHealthServer(server.srv, server)
proto.RegisterAttachmentsServer(S, &Server{})
health.RegisterHealthServer(S, &Server{})
reflection.Register(server.srv)
reflection.Register(S)
return server
}
func ListenGRPC() error {
func (v *Server) Listen() error {
listener, err := net.Listen("tcp", viper.GetString("grpc_bind"))
if err != nil {
return err
}
return S.Serve(listener)
return v.srv.Serve(listener)
}

View File

@@ -0,0 +1,41 @@
package grpc
import (
"context"
"git.solsynth.dev/hypernet/nexus/pkg/nex"
jsoniter "github.com/json-iterator/go"
"git.solsynth.dev/hypernet/nexus/pkg/proto"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
)
func (v *Server) BroadcastEvent(ctx context.Context, in *proto.EventInfo) (*proto.EventResponse, error) {
switch in.GetEvent() {
case "deletion":
data := nex.DecodeMap(in.GetData())
resType, ok := data["type"].(string)
if !ok {
break
}
switch resType {
case "account":
var data struct {
ID int `json:"id"`
}
if err := jsoniter.Unmarshal(in.GetData(), &data); err != nil {
break
}
tx := database.C.Begin()
for _, model := range database.AutoMaintainRange {
switch model.(type) {
default:
tx.Delete(model, "account_id = ?", data.ID)
}
}
tx.Commit()
}
}
return &proto.EventResponse{}, nil
}

View File

@@ -1,18 +0,0 @@
package models
// Account profiles basically fetched from Hydrogen.Passport
// But cache at here for better usage
// At the same time this model can make relations between local models
type Account struct {
BaseModel
Name string `json:"name"`
Nick string `json:"nick"`
Avatar string `json:"avatar"`
Banner string `json:"banner"`
Description string `json:"description"`
EmailAddress string `json:"email_address"`
PowerLevel int `json:"power_level"`
Attachments []Attachment `json:"attachments"`
ExternalID uint `json:"external_id"`
}

View File

@@ -1,22 +0,0 @@
package models
import "gorm.io/datatypes"
type Attachment struct {
BaseModel
Uuid string `json:"uuid"`
Size int64 `json:"size"`
Name string `json:"name"`
Alternative string `json:"alt"`
Usage string `json:"usage"`
MimeType string `json:"mimetype"`
HashCode string `json:"hash"`
Destination string `json:"destination"`
Metadata datatypes.JSONMap `json:"metadata"`
IsMature bool `json:"is_mature"`
Account Account `json:"account"`
AccountID uint `json:"account_id"`
}

View File

@@ -1,17 +0,0 @@
package models
import (
"time"
"gorm.io/datatypes"
"gorm.io/gorm"
)
type JSONMap = datatypes.JSONType[map[string]any]
type BaseModel struct {
ID uint `json:"id" gorm:"primaryKey"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
}

View File

@@ -1,27 +0,0 @@
package models
const (
DestinationTypeLocal = "local"
DestinationTypeS3 = "s3"
)
type BaseDestination struct {
Type string `json:"type"`
}
type LocalDestination struct {
BaseDestination
Path string `json:"path"`
}
type S3Destination struct {
BaseDestination
Path string `json:"path"`
Bucket string `json:"bucket"`
Endpoint string `json:"endpoint"`
SecretID string `json:"secret_id"`
SecretKey string `json:"secret_key"`
EnableSSL bool `json:"enable_ssl"`
}

View File

@@ -1,4 +0,0 @@
package models
type MediaMetadata struct {
}

View File

@@ -2,68 +2,80 @@ package api
import (
"fmt"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/gap"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/server/exts"
"net/url"
"path/filepath"
"strings"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/models"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
"github.com/samber/lo"
"github.com/spf13/viper"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server/exts"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func openAttachment(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id", 0)
func getBillingStatus(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("nex_user").(*sec.UserInfo)
metadata, err := services.GetAttachmentByID(uint(id))
currentBytes, err := services.GetLastDayUploadedBytes(user.ID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
discountFileSize := viper.GetInt64("payment.discount")
return c.JSON(fiber.Map{
"current_bytes": currentBytes,
"discount_file_size": discountFileSize,
"included_ratio": float64(currentBytes) / float64(discountFileSize),
})
}
func openAttachment(c *fiber.Ctx) error {
id := c.Params("id")
region := c.Query("region")
var err error
var url, mimetype string
var filesize int64
size := lo.Ternary(c.QueryBool("preview", true), 1024, -1)
if len(region) > 0 {
url, filesize, mimetype, err = services.OpenAttachmentByRID(id, size, region)
} else {
url, filesize, mimetype, err = services.OpenAttachmentByRID(id, size)
}
destMap := viper.GetStringMap("destinations")
dest, destOk := destMap[metadata.Destination]
if !destOk {
return fiber.NewError(fiber.StatusInternalServerError, "invalid destination: destination configuration was not found")
authenticated := false
if err := sec.EnsureAuthenticated(c); err == nil {
authenticated = true
}
var destParsed models.BaseDestination
rawDest, _ := jsoniter.Marshal(dest)
_ = jsoniter.Unmarshal(rawDest, &destParsed)
switch destParsed.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
if len(metadata.MimeType) > 0 {
c.Set(fiber.HeaderContentType, metadata.MimeType)
}
return c.SendFile(filepath.Join(destConfigured.Path, metadata.Uuid), false)
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
protocol := lo.Ternary(destConfigured.EnableSSL, "https", "http")
return c.Redirect(fmt.Sprintf(
"%s://%s.%s/%s",
protocol,
destConfigured.Bucket,
destConfigured.Endpoint,
url.QueryEscape(filepath.Join(destConfigured.Path, metadata.Uuid)),
))
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", destParsed.Type)
if filesize > viper.GetInt64("traffic.maximum_size") && !authenticated {
return fiber.NewError(fiber.StatusForbidden, "file is too large, you need authorized to access")
}
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
c.Set(fiber.HeaderContentType, mimetype)
if strings.HasPrefix(url, "file://") {
fp := strings.Replace(url, "file://", "", 1)
return c.SendFile(fp)
}
return c.Redirect(url, fiber.StatusFound)
}
func getAttachmentMeta(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id")
id := c.Params("id")
metadata, err := services.GetAttachmentByID(uint(id))
metadata, err := services.GetAttachmentByRID(id)
if err != nil {
return fiber.NewError(fiber.StatusNotFound)
}
@@ -71,75 +83,16 @@ func getAttachmentMeta(c *fiber.Ctx) error {
return c.JSON(metadata)
}
func createAttachment(c *fiber.Ctx) error {
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
destName := c.Query("destination", viper.GetString("preferred_destination"))
hash := c.FormValue("hash")
if len(hash) != 64 {
return fiber.NewError(fiber.StatusBadRequest, "please provide a sha-256 hash code, length should be 64 characters")
}
usage := c.FormValue("usage")
if !lo.Contains(viper.GetStringSlice("accepts_usage"), usage) {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("disallowed usage: %s", usage))
}
file, err := c.FormFile("file")
if err != nil {
return err
}
if err := gap.H.EnsureGrantedPerm(c, "CreateAttachments", file.Size); err != nil {
return err
}
usermeta := make(map[string]any)
_ = jsoniter.UnmarshalFromString(c.FormValue("metadata"), &usermeta)
tx := database.C.Begin()
metadata, linked, err := services.NewAttachmentMetadata(tx, user, file, models.Attachment{
Usage: usage,
HashCode: hash,
Alternative: c.FormValue("alt"),
MimeType: c.FormValue("mimetype"),
Metadata: usermeta,
IsMature: len(c.FormValue("mature")) > 0,
Destination: destName,
})
if err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if !linked {
if err := services.UploadFile(destName, c, file, metadata); err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
}
tx.Commit()
return c.JSON(metadata)
}
func updateAttachmentMeta(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id", 0)
if err := gap.H.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("user").(models.Account)
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Thumbnail *uint `json:"thumbnail"`
Compressed *uint `json:"compressed"`
Alternative string `json:"alt"`
Usage string `json:"usage"`
Metadata map[string]any `json:"metadata"`
IsMature bool `json:"is_mature"`
IsIndexable bool `json:"is_indexable"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
@@ -147,17 +100,52 @@ func updateAttachmentMeta(c *fiber.Ctx) error {
}
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
BaseModel: models.BaseModel{ID: uint(id)},
AccountID: user.ID,
}).First(&attachment).Error; err != nil {
if err := database.C.
Where("id = ? AND account_id = ?", id, user.ID).
Preload("Thumbnail").
Preload("Compressed").
First(&attachment).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if data.Thumbnail != nil && attachment.ThumbnailID != data.Thumbnail {
var thumbnail models.Attachment
if err := database.C.
Where("id = ? AND account_id = ?", data.Thumbnail, user.ID).
First(&thumbnail).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable find thumbnail: %v", err))
}
if attachment.Thumbnail != nil {
services.UnsetAttachmentAsThumbnail(*attachment.Thumbnail)
}
thumbnail, err := services.SetAttachmentAsThumbnail(thumbnail)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable set thumbnail: %v", err))
}
attachment.Thumbnail = &thumbnail
attachment.ThumbnailID = &thumbnail.ID
}
if data.Compressed != nil && attachment.CompressedID != data.Compressed {
var compressed models.Attachment
if err := database.C.
Where("id = ? AND account_id = ?", data.Compressed, user.ID).
First(&compressed).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable find compressed: %v", err))
}
if attachment.Compressed != nil {
services.UnsetAttachmentAsCompressed(*attachment.Compressed)
}
compressed, err := services.SetAttachmentAsCompressed(compressed)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable set compressed: %v", err))
}
attachment.Compressed = &compressed
attachment.CompressedID = &compressed.ID
}
attachment.Alternative = data.Alternative
attachment.Usage = data.Usage
attachment.Metadata = data.Metadata
attachment.IsMature = data.IsMature
attachment.Usermeta = data.Metadata
attachment.IsIndexable = data.IsIndexable
if attachment, err := services.UpdateAttachment(attachment); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
@@ -166,13 +154,41 @@ func updateAttachmentMeta(c *fiber.Ctx) error {
}
}
func deleteAttachment(c *fiber.Ctx) error {
func updateAttachmentRating(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id", 0)
user := c.Locals("nex_user").(*sec.UserInfo)
if err := gap.H.EnsureAuthenticated(c); err != nil {
var data struct {
ContentRating int `json:"content_rating" validate:"required,min=3,max=21"`
QualityRating int `json:"quality_rating" validate:"min=0,max=5"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
user := c.Locals("user").(models.Account)
attachment, err := services.GetAttachmentByID(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else if attachment.AccountID != user.ID {
if err = sec.EnsureGrantedPerm(c, "OverrideAttachmentRating", true); err != nil {
return err
}
}
attachment.ContentRating = data.ContentRating
attachment.QualityRating = data.QualityRating
if attachment, err = services.UpdateAttachment(attachment); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(attachment)
}
}
func deleteAttachment(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id", 0)
user := c.Locals("nex_user").(*sec.UserInfo)
attachment, err := services.GetAttachmentByID(uint(id))
if err != nil {

View File

@@ -0,0 +1,141 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server/exts"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func listBoostByUser(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
if take > 100 {
take = 100
}
count, err := services.CountBoostByUser(user.ID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
boosts, err := services.ListBoostByUser(user.ID, take, offset)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": boosts,
})
}
func listBoostByAttachment(c *fiber.Ctx) error {
attachmentId, _ := c.ParamsInt("attachmentId", 0)
if boost, err := services.ListBoostByAttachment(uint(attachmentId)); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.JSON(boost)
}
}
func getBoost(c *fiber.Ctx) error {
boostId, _ := c.ParamsInt("boostId", 0)
if boost, err := services.GetBoostByID(uint(boostId)); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.JSON(boost)
}
}
func createBoost(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Attachment uint `json:"attachment" validate:"required"`
Destination int `json:"destination" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
var attachment models.Attachment
if err := database.C.Where("id = ?", data.Attachment).First(&attachment).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if boost, err := services.CreateBoost(user, attachment, data.Destination); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(boost)
}
}
func activateBoost(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
boostId, _ := c.ParamsInt("boostId", 0)
boost, err := services.GetBoostByID(uint(boostId))
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else if boost.AccountID != user.ID {
return fiber.NewError(fiber.StatusNotFound, "record not created by you")
}
if err := services.ActivateBoost(boost); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(boost)
}
}
func updateBoost(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
boostId, _ := c.ParamsInt("boostId", 0)
var data struct {
Status int `json:"status" validate:"required"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
boost, err := services.GetBoostByID(uint(boostId))
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else if boost.AccountID != user.ID {
return fiber.NewError(fiber.StatusNotFound, "record not created by you")
}
if boost, err := services.UpdateBoostStatus(boost, data.Status); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(boost)
}
}
func deleteBoost(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
boostId, _ := c.ParamsInt("boostId", 0)
boost, err := services.GetBoostByID(uint(boostId))
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else if boost.AccountID != user.ID {
return fiber.NewError(fiber.StatusNotFound, "record not created by you")
}
if err := services.DeleteBoost(boost); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
return c.SendStatus(fiber.StatusOK)
}
}

View File

@@ -0,0 +1,19 @@
package api
import (
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
)
func listDestination(c *fiber.Ctx) error {
var destinations []models.BaseDestination
for _, value := range services.DestinationsByIndex {
var parsed models.BaseDestination
_ = jsoniter.Unmarshal(value.Raw, &parsed)
parsed.ID = value.Index
destinations = append(destinations, parsed)
}
return c.JSON(destinations)
}

View File

@@ -1,16 +1,77 @@
package api
import "github.com/gofiber/fiber/v2"
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"github.com/gofiber/fiber/v2"
)
func MapAPIs(app *fiber.App) {
app.Get("/.well-known/destinations", getDestinations)
api := app.Group("/api").Name("API")
func MapAPIs(app *fiber.App, baseURL string) {
api := app.Group(baseURL).Name("API")
{
api.Get("/attachments/:id/meta", getAttachmentMeta)
api.Get("/attachments/:id", openAttachment)
api.Post("/attachments", createAttachment)
api.Put("/attachments/:id", updateAttachmentMeta)
api.Delete("/attachments/:id", deleteAttachment)
api.Get("/destinations", listDestination)
api.Get("/billing", getBillingStatus)
boost := api.Group("/boosts").Name("Boosts API")
{
boost.Get("/", listBoostByUser)
boost.Get("/:boostId", getBoost)
boost.Post("/", sec.ValidatorMiddleware, createBoost)
boost.Post("/:boostId/activate", sec.ValidatorMiddleware, activateBoost)
boost.Put("/:boostId", sec.ValidatorMiddleware, updateBoost)
}
pools := api.Group("/pools").Name("Pools API")
{
pools.Get("/", listPool)
pools.Get("/:id", getPool)
pools.Post("/", sec.ValidatorMiddleware, createPool)
pools.Put("/:id", sec.ValidatorMiddleware, updatePool)
pools.Delete("/:id", sec.ValidatorMiddleware, deletePool)
}
attachments := api.Group("/attachments").Name("Attachments API")
{
attachments.Get("/:attachmentId/boosts", listBoostByAttachment)
attachments.Get("/", listAttachment)
attachments.Get("/:id/meta", getAttachmentMeta)
attachments.Get("/:id", openAttachment)
attachments.Post("/", sec.ValidatorMiddleware, createAttachmentDirectly)
attachments.Post("/referenced", sec.ValidatorMiddleware, createAttachmentWithURL)
attachments.Put("/:id", sec.ValidatorMiddleware, updateAttachmentMeta)
attachments.Put("/:id/rating", sec.ValidatorMiddleware, updateAttachmentRating)
attachments.Delete("/:id", sec.ValidatorMiddleware, deleteAttachment)
}
fragments := api.Group("/fragments").Name("Fragments API")
{
fragments.Post("/", sec.ValidatorMiddleware, createAttachmentFragment)
fragments.Post("/:file/:chunk", sec.ValidatorMiddleware, uploadFragmentChunk)
}
stickers := api.Group("/stickers").Name("Stickers API")
{
packs := stickers.Group("/packs").Name("Sticker Packs API")
{
packs.Get("/", listStickerPacks)
packs.Get("/own", listOwnedStickerPacks)
packs.Get("/:packId", getStickerPack)
packs.Post("/", sec.ValidatorMiddleware, createStickerPack)
packs.Put("/:packId", sec.ValidatorMiddleware, updateStickerPack)
packs.Delete("/:packId", sec.ValidatorMiddleware, deleteStickerPack)
packs.Post("/:packId/own", addStickerPack)
packs.Delete("/:packId/own", removeStickerPack)
}
stickers.Get("/lookup", lookupStickerBatch)
stickers.Get("/lookup/:alias", getStickerByAlias)
stickers.Get("/lookup/:alias/open", openStickerByAlias)
stickers.Get("/", listStickers)
stickers.Get("/:stickerId", getSticker)
stickers.Post("/", sec.ValidatorMiddleware, createSticker)
stickers.Put("/:stickerId", sec.ValidatorMiddleware, updateSticker)
stickers.Delete("/:stickerId", sec.ValidatorMiddleware, deleteSticker)
}
}
}

View File

@@ -0,0 +1,123 @@
package api
import (
"fmt"
"strings"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
"github.com/spf13/viper"
"gorm.io/datatypes"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func listAttachment(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
if take > 100 {
take = 100
}
tx := database.C
needQuery := true
result := make([]models.Attachment, take)
var idxList []string
if len(c.Query("id")) > 0 {
var pendingQueryId []string
idx := strings.Split(c.Query("id"), ",")
for p, raw := range idx {
idxList = append(idxList, raw)
if val, ok := services.GetAttachmentCache(raw); ok {
result[p] = val
} else {
pendingQueryId = append(pendingQueryId, raw)
}
}
tx = tx.Where("rid IN ?", pendingQueryId)
needQuery = len(pendingQueryId) > 0
} else {
tx = tx.Where("is_indexable IS NULL OR is_indexable = ?", true)
// Do sort this when doesn't filter by the id
// Because the sort will mess up the result
tx = tx.Order("created_at DESC")
// Do not expose un-public indexable attachments
prefix := viper.GetString("database.prefix")
tx = tx.
Joins(fmt.Sprintf("JOIN %sattachment_pools ON %sattachment_pools.id = %sattachments.pool_id", prefix, prefix, prefix)).
Where(datatypes.JSONQuery(fmt.Sprintf("%sattachment_pools.config", prefix)).Equals(true, "public_indexable"))
}
if len(c.Query("author")) > 0 {
author, err := authkit.GetUserByName(gap.Nx, c.Query("author"))
if err == nil {
tx = tx.Where("attachments.account_id = ?", author.ID)
}
}
if pools := c.Query("pools"); len(pools) > 0 {
prefix := viper.GetString("database.prefix")
poolAliases := strings.Split(pools, ",")
tx = tx.
Joins(fmt.Sprintf("JOIN %sattachment_pools ON %sattachment_pools.id = %sattachments.pool_id", prefix, prefix, prefix)).
Where(fmt.Sprintf("%sattachment_pools.alias IN ?", prefix), poolAliases)
}
if original := c.QueryBool("original", false); original {
tx = tx.Where("ref_id IS NULL")
}
var count int64
countTx := tx
if err := countTx.Model(&models.Attachment{}).Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if needQuery {
var out []models.Attachment
if err := tx.
Offset(offset).Limit(take).
Preload("Pool").
Preload("Thumbnail").
Preload("Compressed").
Preload("Boosts").
Find(&out).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if len(idxList) == 0 {
result = out
} else {
for _, item := range out {
for p, id := range idxList {
if item.Rid == id {
result[p] = item
}
}
}
}
}
for _, item := range result {
services.CacheAttachment(item)
}
out, err := services.CompleteAttachmentMeta(result...)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": out,
})
}

View File

@@ -0,0 +1,104 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server/exts"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
"gorm.io/datatypes"
)
func listPool(c *fiber.Ctx) error {
pools, err := services.ListAttachmentPool()
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(pools)
}
func getPool(c *fiber.Ctx) error {
id, _ := c.ParamsInt("id")
pool, err := services.GetAttachmentPool(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(pool)
}
func createPool(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Alias string `json:"alias" validate:"required"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Config models.AttachmentPoolConfig `json:"config"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
pool := models.AttachmentPool{
Alias: data.Alias,
Name: data.Name,
Description: data.Description,
Config: datatypes.NewJSONType(data.Config),
AccountID: &user.ID,
}
if pool, err := services.NewAttachmentPool(pool); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(pool)
}
}
func updatePool(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Alias string `json:"alias" validate:"required"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
Config models.AttachmentPoolConfig `json:"config"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
id, _ := c.ParamsInt("id")
pool, err := services.GetAttachmentPoolWithUser(uint(id), user.ID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
pool.Alias = data.Alias
pool.Name = data.Name
pool.Description = data.Description
pool.Config = datatypes.NewJSONType(data.Config)
if pool, err := services.UpdateAttachmentPool(pool); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(pool)
}
}
func deletePool(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
id, _ := c.ParamsInt("id")
pool, err := services.GetAttachmentPoolWithUser(uint(id), user.ID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if pool, err := services.DeleteAttachmentPool(pool); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
return c.JSON(pool)
}
}

View File

@@ -0,0 +1,195 @@
package api
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server/exts"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
"github.com/gofiber/fiber/v2"
"github.com/samber/lo"
)
func listStickerPacks(c *fiber.Ctx) error {
take := c.QueryInt("take", 0)
offset := c.QueryInt("offset", 0)
if take > 100 {
take = 100
}
tx := database.C
if len(c.Query("author")) > 0 {
author, err := authkit.GetUserByName(gap.Nx, c.Query("author"))
if err == nil {
tx = tx.Where("account_id = ?", author.ID)
}
}
var count int64
if err := database.C.Model(&models.StickerPack{}).Count(&count).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
var packs []models.StickerPack
if err := tx.Limit(take).Offset(offset).Find(&packs).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(fiber.Map{
"count": count,
"data": packs,
})
}
func listOwnedStickerPacks(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("nex_user").(*sec.UserInfo)
var ownerships []models.StickerPackOwnership
if err := database.C.Where("account_id = ?", user.ID).Find(&ownerships).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
idSet := lo.Map(ownerships, func(o models.StickerPackOwnership, _ int) uint {
return o.PackID
})
var packs []models.StickerPack
if err := database.C.Where("id IN ?", idSet).Find(&packs).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(packs)
}
func getStickerPack(c *fiber.Ctx) error {
id, _ := c.ParamsInt("packId", 0)
pack, err := services.GetStickerPack(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
var stickers []models.Sticker
if err := database.C.Where("pack_id = ?", pack.ID).
Preload("Attachment").
Find(&stickers).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
pack.Stickers = stickers
}
return c.JSON(pack)
}
func createStickerPack(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Prefix string `json:"prefix" validate:"required,alphanum,min=2,max=12"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
pack, err := services.NewStickerPack(user, data.Prefix, data.Name, data.Description)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(pack)
}
func updateStickerPack(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Prefix string `json:"prefix" validate:"required,alphanum,min=2,max=12"`
Name string `json:"name" validate:"required"`
Description string `json:"description"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
id, _ := c.ParamsInt("packId", 0)
pack, err := services.GetStickerPackWithUser(uint(id), user.ID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
pack.Prefix = data.Prefix
pack.Name = data.Name
pack.Description = data.Description
if pack, err = services.UpdateStickerPack(pack); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(pack)
}
func deleteStickerPack(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
id, _ := c.ParamsInt("packId", 0)
pack, err := services.GetStickerPackWithUser(uint(id), user.ID)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if pack, err = services.DeleteStickerPack(pack); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(pack)
}
func addStickerPack(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("nex_user").(*sec.UserInfo)
packId, _ := c.ParamsInt("packId", 0)
var pack models.StickerPack
if err := database.C.Where("id = ?", packId).First(&pack).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
ownership, err := services.AddStickerPack(user.ID, pack)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(ownership)
}
func removeStickerPack(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("nex_user").(*sec.UserInfo)
packId, _ := c.ParamsInt("packId", 0)
var pack models.StickerPack
if err := database.C.Where("id = ?", packId).First(&pack).Error; err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
ownership, err := services.RemoveStickerPack(user.ID, pack)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(ownership)
}

View File

@@ -0,0 +1,206 @@
package api
import (
"fmt"
"strings"
"github.com/samber/lo"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server/exts"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func lookupStickerBatch(c *fiber.Ctx) error {
probe := c.Query("probe")
if stickers, err := services.GetStickerLikeAlias(probe); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else {
return c.JSON(stickers)
}
}
func getStickerByAlias(c *fiber.Ctx) error {
alias := c.Params("alias")
if sticker, err := services.GetStickerWithAlias(alias); err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
} else {
return c.JSON(sticker)
}
}
func openStickerByAlias(c *fiber.Ctx) error {
alias := c.Params("alias")
region := c.Query("region")
sticker, err := services.GetStickerWithAlias(alias)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
var url, mimetype string
if len(region) > 0 {
url, _, mimetype, err = services.OpenAttachmentByRID(sticker.Attachment.Rid, 256, region)
} else {
url, _, mimetype, err = services.OpenAttachmentByRID(sticker.Attachment.Rid, 288)
}
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
c.Set(fiber.HeaderContentType, mimetype)
if strings.HasPrefix(url, "file://") {
fp := strings.Replace(url, "file://", "", 1)
return c.SendFile(fp)
}
return c.Redirect(url, fiber.StatusFound)
}
func listStickers(c *fiber.Ctx) error {
if err := sec.EnsureAuthenticated(c); err != nil {
return err
}
user := c.Locals("nex_user").(*sec.UserInfo)
var ownerships []models.StickerPackOwnership
if err := database.C.Where("account_id = ?", user.ID).Find(&ownerships).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
tx := database.C.Where("pack_id IN ?", lo.Map(ownerships, func(o models.StickerPackOwnership, _ int) uint {
return o.PackID
}))
var stickers []models.Sticker
if err := tx.
Preload("Attachment").Preload("Pack").
Find(&stickers).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
return c.JSON(stickers)
}
func getSticker(c *fiber.Ctx) error {
id, _ := c.ParamsInt("stickerId", 0)
sticker, err := services.GetSticker(uint(id))
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
return c.JSON(sticker)
}
func createSticker(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Alias string `json:"alias" validate:"required,alphanum,min=2,max=12"`
Name string `json:"name" validate:"required"`
AttachmentID string `json:"attachment_id"`
PackID uint `json:"pack_id"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
var attachment models.Attachment
if err := database.C.Where("rid = ?", data.AttachmentID).First(&attachment).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find attachment: %v", err))
} else if !attachment.IsAnalyzed {
return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be analyzed")
}
if strings.SplitN(attachment.MimeType, "/", 2)[0] != "image" {
return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be an image")
}
var pack models.StickerPack
if err := database.C.Where("id = ? AND account_id = ?", data.PackID, user.ID).First(&pack).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find pack: %v", err))
}
sticker, err := services.NewSticker(models.Sticker{
Alias: data.Alias,
Name: data.Name,
Attachment: attachment,
AccountID: user.ID,
PackID: pack.ID,
AttachmentID: attachment.ID,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(sticker)
}
func updateSticker(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Alias string `json:"alias" validate:"required,alphanum,min=2,max=12"`
Name string `json:"name" validate:"required"`
AttachmentID string `json:"attachment_id"`
PackID uint `json:"pack_id"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
id, _ := c.ParamsInt("stickerId", 0)
sticker, err := services.GetStickerWithUser(uint(id), user.ID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
var attachment models.Attachment
if err := database.C.Where("rid = ?", data.AttachmentID).First(&attachment).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find attachment: %v", err))
} else if !attachment.IsAnalyzed {
return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be analyzed")
}
if strings.SplitN(attachment.MimeType, "/", 2)[0] != "image" {
return fiber.NewError(fiber.StatusBadRequest, "sticker attachment must be an image")
}
var pack models.StickerPack
if err := database.C.Where("id = ? AND account_id = ?", data.PackID, user.ID).First(&pack).Error; err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find pack: %v", err))
}
sticker.Alias = data.Alias
sticker.Name = data.Name
sticker.PackID = data.PackID
sticker.AttachmentID = attachment.ID
if sticker, err = services.UpdateSticker(sticker); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(sticker)
}
func deleteSticker(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
id, _ := c.ParamsInt("stickerId", 0)
sticker, err := services.GetStickerWithUser(uint(id), user.ID)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, err.Error())
}
if sticker, err = services.DeleteSticker(sticker); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(sticker)
}

View File

@@ -0,0 +1,131 @@
package api
import (
"fmt"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server/exts"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
"github.com/spf13/viper"
)
func createAttachmentDirectly(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
poolAlias := c.FormValue("pool")
aliasingMap := viper.GetStringMapString("pools.aliases")
if val, ok := aliasingMap[poolAlias]; ok {
poolAlias = val
}
pool, err := services.GetAttachmentPoolByAlias(poolAlias)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get attachment pool info: %v", err))
}
file, err := c.FormFile("file")
if err != nil {
return err
}
if !user.HasPermNode("CreateAttachments", true) {
return fiber.NewError(fiber.StatusForbidden, "you are not permitted to create attachments")
} else if pool.Config.Data().MaxFileSize != nil && file.Size > *pool.Config.Data().MaxFileSize {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment pool %s doesn't allow file larger than %d", pool.Alias, *pool.Config.Data().MaxFileSize))
}
usermeta := make(map[string]any)
_ = jsoniter.UnmarshalFromString(c.FormValue("metadata"), &usermeta)
tx := database.C.Begin()
metadata, err := services.NewAttachmentMetadata(tx, user, file, models.Attachment{
Alternative: c.FormValue("alt"),
MimeType: c.FormValue("mimetype"),
Usermeta: usermeta,
IsAnalyzed: false,
Destination: models.AttachmentDstTemporary,
Pool: &pool,
PoolID: &pool.ID,
})
if err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
if err := services.UploadFileToTemporary(c, file, metadata); err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
// If pool has no belongs to, it means it is shared pool, apply shared attachment discount
withDiscount := pool.AccountID == nil
if err := services.PlaceOrder(user.ID, file.Size, withDiscount); err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusPaymentRequired, err.Error())
}
tx.Commit()
metadata.Pool = &pool
if c.QueryBool("analyzeNow", false) {
services.AnalyzeAttachment(metadata)
} else {
services.PublishAnalyzeTask(metadata)
}
return c.JSON(metadata)
}
func createAttachmentWithURL(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
poolAlias := c.FormValue("pool")
aliasingMap := viper.GetStringMapString("pools.aliases")
if val, ok := aliasingMap[poolAlias]; ok {
poolAlias = val
}
var data struct {
URL string `json:"url"`
Metadata map[string]any `json:"metadata"`
Mimetype string `json:"mimetype"`
Name string `json:"filename"`
Alternative string `json:"alt"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
if !user.HasPermNode("CreateReferencedAttachments", true) {
return fiber.NewError(fiber.StatusForbidden, "you are not permitted to create attachments with URL")
}
pool, err := services.GetAttachmentPoolByAlias(poolAlias)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get attachment pool info: %v", err))
}
attachment := models.Attachment{
Name: data.Name,
Alternative: data.Alternative,
MimeType: c.FormValue("mimetype"),
Usermeta: data.Metadata,
IsAnalyzed: true,
Destination: models.AttachmentDstExternal,
Pool: &pool,
PoolID: &pool.ID,
}
if attachment, err = services.NewRefURLAttachment(database.C, user, attachment); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(attachment)
}

View File

@@ -0,0 +1,154 @@
package api
import (
"encoding/json"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/fs"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server/exts"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
)
func createAttachmentFragment(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
var data struct {
Pool string `json:"pool" validate:"required"`
Size int64 `json:"size" validate:"required"`
FileName string `json:"name" validate:"required"`
Alternative string `json:"alt"`
MimeType string `json:"mimetype"`
Fingerprint *string `json:"fingerprint"`
Metadata map[string]any `json:"metadata"`
}
if err := exts.BindAndValidate(c, &data); err != nil {
return err
}
aliasingMap := viper.GetStringMapString("pools.aliases")
if val, ok := aliasingMap[data.Pool]; ok {
data.Pool = val
}
pool, err := services.GetAttachmentPoolByAlias(data.Pool)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to get attachment pool info: %v", err))
}
if !user.HasPermNode("CreateAttachments", true) {
return fiber.NewError(fiber.StatusForbidden, "you are not permitted to create attachments")
} else if pool.Config.Data().MaxFileSize != nil && *pool.Config.Data().MaxFileSize > data.Size {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("attachment pool %s doesn't allow file larger than %d", pool.Alias, *pool.Config.Data().MaxFileSize))
}
tx := database.C.Begin()
metadata, err := services.NewAttachmentFragment(tx, user, models.AttachmentFragment{
Name: data.FileName,
Size: data.Size,
Alternative: data.Alternative,
MimeType: data.MimeType,
Usermeta: data.Metadata,
Fingerprint: data.Fingerprint,
Pool: &pool,
PoolID: &pool.ID,
})
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
} else {
metadata.FileChunksMissing = services.FindFragmentMissingChunks(metadata)
}
// If pool has no belongs to, it means it is shared pool, apply shared attachment discount
withDiscount := pool.AccountID == nil
if err := services.PlaceOrder(user.ID, data.Size, withDiscount); err != nil {
tx.Rollback()
return fiber.NewError(fiber.StatusPaymentRequired, err.Error())
}
tx.Commit()
return c.JSON(fiber.Map{
"chunk_size": viper.GetInt64("performance.file_chunk_size"),
"chunk_count": len(metadata.FileChunks),
"meta": metadata,
})
}
func uploadFragmentChunk(c *fiber.Ctx) error {
user := c.Locals("nex_user").(*sec.UserInfo)
rid := c.Params("file")
cid := c.Params("chunk")
fileData := c.Body()
if len(fileData) == 0 {
return fiber.NewError(fiber.StatusBadRequest, "no file data")
} else if len(fileData) > viper.GetInt("performance.file_chunk_size") {
return fiber.NewError(fiber.StatusBadRequest, "file is too large for one chunk")
}
meta, err := services.GetFragmentByRID(rid)
if err != nil {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("attachment was not found: %v", err))
} else if user.ID != meta.AccountID {
return fiber.NewError(fiber.StatusForbidden, "you are not authorized to upload this attachment")
}
if _, ok := meta.FileChunks[cid]; !ok {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("chunk %s was not found", cid))
} else if services.CheckFragmentChunkExists(meta, cid) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("chunk %s was uploaded", cid))
}
if err := services.UploadFragmentChunkBytes(c, cid, fileData, meta); err != nil {
return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
chunkArrange := make([]string, len(meta.FileChunks))
isAllUploaded := true
for cid, idx := range meta.FileChunks {
if !services.CheckFragmentChunkExists(meta, cid) {
isAllUploaded = false
break
} else if val, ok := idx.(json.Number); ok {
data, _ := val.Int64()
chunkArrange[data] = cid
}
}
if !isAllUploaded {
return c.JSON(fiber.Map{
"is_finished": false,
"fragment": meta,
})
}
// Merge & post-upload
attachment, err := fs.MergeFileChunks(meta, chunkArrange)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
// Post-upload tasks
if err := database.C.Save(&attachment).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
if c.QueryBool("analyzeNow", false) {
services.AnalyzeAttachment(attachment)
} else {
services.PublishAnalyzeTask(attachment)
}
return c.JSON(fiber.Map{
"is_finished": true,
"attachment": attachment,
})
}

View File

@@ -1,18 +0,0 @@
package api
import (
"github.com/gofiber/fiber/v2"
"github.com/spf13/viper"
)
func getDestinations(c *fiber.Ctx) error {
var data []string
for key := range viper.GetStringMap("destinations") {
data = append(data, key)
}
return c.JSON(fiber.Map{
"data": data,
"preferred": viper.GetString("preferred_destination"),
})
}

View File

@@ -1,19 +0,0 @@
package exts
import (
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/services"
"github.com/gofiber/fiber/v2"
)
func LinkAccountMiddleware(c *fiber.Ctx) error {
if val, ok := c.Locals("p_user").(*proto.UserInfo); ok {
if account, err := services.LinkAccount(val); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
} else {
c.Locals("user", account)
}
}
return c.Next()
}

View File

@@ -1,11 +1,12 @@
package server
import (
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/gap"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/server/api"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/server/exts"
"strings"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server/api"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/idempotency"
@@ -15,23 +16,28 @@ import (
"github.com/spf13/viper"
)
var A *fiber.App
var IReader *sec.InternalTokenReader
func NewServer() {
A = fiber.New(fiber.Config{
type App struct {
app *fiber.App
}
func NewServer() *App {
app := fiber.New(fiber.Config{
DisableStartupMessage: true,
EnableIPValidation: true,
ServerHeader: "Hydrogen.Paperclip",
AppName: "Hydrogen.Paperclip",
ServerHeader: "Hypernet.Paperclip",
AppName: "Hypernet.Paperclip",
ProxyHeader: fiber.HeaderXForwardedFor,
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
BodyLimit: 512 * 1024 * 1024 * 1024, // 512 TiB
ReadBufferSize: 5 * 1024 * 1024, // 5MB for large JWT
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
})
A.Use(idempotency.New())
A.Use(cors.New(cors.Config{
app.Use(idempotency.New())
app.Use(cors.New(cors.Config{
AllowCredentials: true,
AllowMethods: strings.Join([]string{
fiber.MethodGet,
@@ -47,19 +53,20 @@ func NewServer() {
},
}))
A.Use(logger.New(logger.Config{
app.Use(logger.New(logger.Config{
Format: "${status} | ${latency} | ${method} ${path}\n",
Output: log.Logger,
}))
A.Use(gap.H.AuthMiddleware)
A.Use(exts.LinkAccountMiddleware)
app.Use(sec.ContextMiddleware(IReader))
api.MapAPIs(A)
api.MapAPIs(app, "/api")
return &App{app}
}
func Listen() {
if err := A.Listen(viper.GetString("bind")); err != nil {
func (v *App) Listen() {
if err := v.app.Listen(viper.GetString("bind")); err != nil {
log.Fatal().Err(err).Msg("An error occurred when starting server...")
}
}

View File

@@ -0,0 +1,378 @@
package services
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"image"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/barasher/go-exiftool"
"github.com/samber/lo"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/fs"
jsoniter "github.com/json-iterator/go"
"github.com/k0kubun/go-ansi"
"github.com/kettek/apng"
"github.com/rs/zerolog/log"
"github.com/schollz/progressbar/v3"
"github.com/spf13/viper"
"golang.org/x/image/webp"
"gopkg.in/vansante/go-ffprobe.v2"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
)
var fileAnalyzeQueue = make(chan models.Attachment, 256)
func PublishAnalyzeTask(file models.Attachment) {
fileAnalyzeQueue <- file
}
func StartConsumeAnalyzeTask() {
for {
task := <-fileAnalyzeQueue
start := time.Now()
if err := AnalyzeAttachment(task); err != nil {
log.Error().Err(err).Any("task", task).Msg("A file analyze task failed...")
} else {
log.Info().Dur("elapsed", time.Since(start)).Uint("id", task.ID).Msg("A file analyze task was completed.")
}
}
}
func ScanUnanalyzedFileFromDatabase() {
workers := viper.GetInt("workers.files_analyze")
if workers < 2 {
log.Warn().Int("val", workers).Int("min", 2).Msg("The file analyzer does not have enough computing power, and the scan of unanalyzed files will not start...")
}
var attachments []models.Attachment
if err := database.C.
Where("is_uploaded = ?", true).
Where("destination = ? OR is_analyzed = ?", models.AttachmentDstTemporary, false).
Find(&attachments).Error; err != nil {
log.Error().Err(err).Msg("Scan unanalyzed files from database failed...")
return
}
if len(attachments) == 0 {
return
}
go func() {
var deletionIdSet []uint
bar := progressbar.NewOptions(len(attachments),
progressbar.OptionSetWriter(ansi.NewAnsiStdout()),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowBytes(true),
progressbar.OptionSetWidth(15),
progressbar.OptionSetDescription("Analyzing the unanalyzed files..."),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "[green]=[reset]",
SaucerHead: "[green]>[reset]",
SaucerPadding: " ",
BarStart: "[",
BarEnd: "]",
}))
for _, task := range attachments {
if err := AnalyzeAttachment(task); err != nil {
log.Error().Err(err).Any("task", task).Msg("A background file analyze task failed...")
deletionIdSet = append(deletionIdSet, task.ID)
}
bar.Add(1)
}
log.Info().Int("count", len(attachments)).Int("fails", len(deletionIdSet)).Msg("All unanalyzed files has been analyzed!")
if len(deletionIdSet) > 0 {
database.C.Delete(&models.Attachment{}, deletionIdSet)
}
}()
}
func parseExifOrientation(src string) int {
switch src {
case "Horizontal":
return 1
case "Mirror horizontal":
return 2
case "Rotate 180":
return 3
case "Mirror vertical":
return 4
case "Mirror horizontal and rotate 270 CW":
return 5
case "Rotate 90 CW":
return 6
case "Mirror horizontal and rotate 90 CW":
return 7
case "Rotate 270 CW":
return 8
default:
return 0
}
}
func calculateAspectRatio(width, height int, orientation int) float64 {
switch orientation {
case 5, 6, 7, 8:
width, height = height, width
}
return float64(width) / float64(height)
}
func AnalyzeAttachment(file models.Attachment) error {
if file.Destination != models.AttachmentDstTemporary {
return fmt.Errorf("attachment isn't in temporary storage, unable to analyze")
}
var start time.Time
if len(file.HashCode) == 0 {
if hash, err := HashAttachment(file); err != nil {
return err
} else {
file.HashCode = hash
}
}
// Do analyze jobs
if !file.IsAnalyzed || len(file.HashCode) == 0 {
destMap := viper.GetStringMap("destinations.0")
var dest models.LocalDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
start = time.Now()
dst := filepath.Join(dest.Path, file.Uuid)
if _, err := os.Stat(dst); os.IsNotExist(err) {
return fmt.Errorf("attachment doesn't exists in temporary storage: %v", err)
}
exifWhitelist := []string{
"Model", "ShutterSpeed", "ISO", "Megapixels", "Aperture",
"ColorSpace", "ColorTemperature", "ColorTone", "Contrast",
"ExposureTime", "FNumber", "FocalLength", "Flash", "HDREffect",
"LensModel", "Orientation",
}
switch strings.SplitN(file.MimeType, "/", 2)[0] {
case "image":
// Dealing with image
reader, err := os.Open(dst)
if err != nil {
return fmt.Errorf("unable to open file: %v", err)
}
defer reader.Close()
var im image.Image
switch file.MimeType {
case "image/webp":
im, err = webp.Decode(reader)
case "image/apng":
im, err = apng.Decode(reader)
default:
im, _, err = image.Decode(reader)
}
if err != nil {
return fmt.Errorf("unable to decode file as an image: %v", err)
}
width := im.Bounds().Dx()
height := im.Bounds().Dy()
ratio := float64(width) / float64(height)
file.Metadata = map[string]any{
"width": width,
"height": height,
"ratio": ratio,
"exif": map[string]any{},
}
// Removing location EXIF data
et, err := exiftool.NewExiftool()
if err == nil {
defer et.Close()
exif := et.ExtractMetadata(dst)
for _, data := range exif {
for k := range data.Fields {
if k == "Orientation" {
ori := parseExifOrientation(data.Fields[k].(string))
file.Metadata["ratio"] = calculateAspectRatio(width, height, ori)
}
if strings.HasPrefix(k, "GPS") {
data.Clear(k)
} else if lo.Contains(exifWhitelist, k) {
file.Metadata["exif"].(map[string]any)[k] = data.Fields[k]
}
}
}
et.WriteMetadata(exif)
}
case "video":
// Dealing with video
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
data, err := ffprobe.ProbeURL(ctx, dst)
if err != nil {
return fmt.Errorf("unable to analyze video information: %v", err)
}
stream := data.FirstVideoStream()
ratio := float64(stream.Width) / float64(stream.Height)
duration, _ := strconv.ParseFloat(stream.Duration, 64)
file.Metadata = map[string]any{
"width": stream.Width,
"height": stream.Height,
"ratio": ratio,
"duration": duration,
"bit_rate": stream.BitRate,
"codec_name": stream.CodecName,
"color_range": stream.ColorRange,
"color_space": stream.ColorSpace,
"exif": map[string]any{},
}
// Removing location EXIF data
et, err := exiftool.NewExiftool()
if err == nil {
defer et.Close()
exif := et.ExtractMetadata(dst)
for _, data := range exif {
for k := range data.Fields {
if k == "Orientation" {
ori := parseExifOrientation(data.Fields[k].(string))
file.Metadata["ratio"] = calculateAspectRatio(stream.Width, stream.Height, ori)
}
if strings.HasPrefix(k, "GPS") {
data.Clear(k)
} else if lo.Contains(exifWhitelist, k) {
file.Metadata["exif"].(map[string]any)[k] = data.Fields[k]
}
}
}
et.WriteMetadata(exif)
}
}
}
tx := database.C.Begin()
if err := tx.Model(&file).Updates(&models.Attachment{
IsAnalyzed: true,
Metadata: file.Metadata,
HashCode: file.HashCode,
}).Error; err != nil {
tx.Rollback()
return fmt.Errorf("unable to update file record: %v", err)
}
linked, err := TryLinkAttachment(tx, file, file.HashCode)
if linked && err != nil {
return fmt.Errorf("unable to link file record: %v", err)
}
tx.Commit()
log.Info().Dur("elapsed", time.Since(start)).Uint("id", file.ID).Msg("A file analyze task was finished, starting uploading...")
// Move temporary to permanent
if !linked {
go func() {
start = time.Now()
preferred := viper.GetInt("preferred_destination")
if preferred == 0 {
preferred = 1
}
if err := ReUploadFile(file, preferred); err != nil {
log.Warn().Any("file", file).Err(err).Msg("Unable to move file to permanet storage...")
} else {
// Recycle the temporary file
file.Destination = models.AttachmentDstTemporary
go fs.DeleteFile(file)
// Finish
log.Info().Dur("elapsed", time.Since(start)).Uint("id", file.ID).Msg("A file post-analyze upload task was finished.")
}
}()
} else {
log.Info().Uint("id", file.ID).Msg("File is linked to exists one, skipping uploading...")
}
return nil
}
func HashAttachment(file models.Attachment) (hash string, err error) {
const chunkSize = 32 * 1024
destMap := viper.GetStringMapString("destinations.0")
destPath := filepath.Join(destMap["path"], file.Uuid)
// Check if the file exists
fileInfo, err := os.Stat(destPath)
if os.IsNotExist(err) {
return "", fmt.Errorf("file does not exist: %v", err)
}
// Open the file
inFile, err := os.Open(destPath)
if err != nil {
return "", fmt.Errorf("unable to open file: %v", err)
}
defer inFile.Close()
hasher := sha256.New()
if chunkSize*3 > fileInfo.Size() {
// If the total size is smaller than three chunks, then hash the whole file
buf := make([]byte, fileInfo.Size())
if _, err := inFile.Read(buf); err != nil && err != io.EOF {
return "", fmt.Errorf("error reading whole file: %v", err)
}
hasher.Write(buf)
} else {
// Hash the first 32KB
buf := make([]byte, chunkSize)
if _, err := inFile.Read(buf); err != nil && err != io.EOF {
return "", fmt.Errorf("error reading file: %v", err)
}
hasher.Write(buf)
// Hash the middle 32KB
middleOffset := fileInfo.Size() / 2
if _, err := inFile.Seek(middleOffset, io.SeekStart); err != nil {
return "", fmt.Errorf("error seeking to middle: %v", err)
}
if _, err := inFile.Read(buf); err != nil && err != io.EOF {
return "", fmt.Errorf("error reading middle: %v", err)
}
hasher.Write(buf)
// Hash the last 32KB
endOffset := fileInfo.Size() - chunkSize
if _, err := inFile.Seek(endOffset, io.SeekStart); err != nil {
return "", fmt.Errorf("error seeking to end: %v", err)
}
if _, err := inFile.Read(buf); err != nil && err != io.EOF {
return "", fmt.Errorf("error reading end: %v", err)
}
hasher.Write(buf)
}
// Hash with the file metadata
fmt.Fprintf(hasher, "%d", file.Size)
// Return the combined hash
hash = hex.EncodeToString(hasher.Sum(nil))
return
}

View File

@@ -2,136 +2,345 @@ package services
import (
"fmt"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"mime"
"mime/multipart"
"net/http"
"path/filepath"
"time"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/models"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/passport/pkg/authkit"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/fs"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
amodels "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
"github.com/google/uuid"
"github.com/samber/lo"
"gorm.io/gorm"
)
const metadataCacheLimit = 512
func KgAttachmentCache(rid string) string {
return cachekit.FKey(cachekit.DAAttachment, rid)
}
var metadataCache = make(map[uint]models.Attachment)
func CompleteAttachmentMeta(in ...models.Attachment) ([]models.Attachment, error) {
var usersId []uint
for _, item := range in {
usersId = append(usersId, item.AccountID)
}
usersId = lo.Uniq(usersId)
users, err := authkit.ListUser(gap.Nx, usersId)
if err != nil {
return in, fmt.Errorf("failed to list users: %v", err)
}
for idx, item := range in {
item.Account = lo.FindOrElse(users, amodels.Account{}, func(idx amodels.Account) bool {
return item.AccountID == idx.ID
})
in[idx] = item
}
return in, nil
}
func GetAttachmentByID(id uint) (models.Attachment, error) {
if val, ok := metadataCache[id]; ok {
var attachment models.Attachment
if err := database.C.
Where("id = ?", id).
Preload("Pool").
Preload("Thumbnail").
Preload("Compressed").
Preload("Boosts").
First(&attachment).Error; err != nil {
return attachment, err
} else {
CacheAttachment(attachment)
}
out, err := CompleteAttachmentMeta(attachment)
return out[0], err
}
func GetAttachmentByRID(rid string) (models.Attachment, error) {
if val, err := cachekit.Get[models.Attachment](
gap.Ca,
KgAttachmentCache(rid),
); err == nil {
return val, nil
}
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
BaseModel: models.BaseModel{ID: id},
}).First(&attachment).Error; err != nil {
Rid: rid,
}).
Preload("Pool").
Preload("Thumbnail").
Preload("Compressed").
Preload("Boosts").
First(&attachment).Error; err != nil {
return attachment, err
} else {
if len(metadataCache) > metadataCacheLimit {
clear(metadataCache)
}
metadataCache[id] = attachment
CacheAttachment(attachment)
}
return attachment, nil
out, err := CompleteAttachmentMeta(attachment)
return out[0], err
}
func GetAttachmentByHash(hash string) (models.Attachment, error) {
var attachment models.Attachment
if err := database.C.Where(models.Attachment{
HashCode: hash,
}).First(&attachment).Error; err != nil {
}).Preload("Pool").First(&attachment).Error; err != nil {
return attachment, err
}
return attachment, nil
}
func NewAttachmentMetadata(tx *gorm.DB, user models.Account, file *multipart.FileHeader, attachment models.Attachment) (models.Attachment, bool, error) {
linked := false
exists, pickupErr := GetAttachmentByHash(attachment.HashCode)
if pickupErr == nil {
linked = true
exists.Alternative = attachment.Alternative
exists.Usage = attachment.Usage
exists.Metadata = attachment.Metadata
attachment = exists
attachment.ID = 0
attachment.AccountID = user.ID
} else {
// Upload the new file
attachment.Uuid = uuid.NewString()
attachment.Size = file.Size
attachment.Name = file.Filename
attachment.AccountID = user.ID
func GetAttachmentCache(rid string) (models.Attachment, bool) {
if val, err := cachekit.Get[models.Attachment](
gap.Ca,
KgAttachmentCache(rid),
); err == nil {
return val, true
}
return models.Attachment{}, false
}
// If the user didn't provide file mimetype manually, we have to detect it
if len(attachment.MimeType) == 0 {
if ext := filepath.Ext(attachment.Name); len(ext) > 0 {
// Detect mimetype by file extensions
attachment.MimeType = mime.TypeByExtension(ext)
} else {
// Detect mimetype by file header
// This method as a fallback method, because this isn't pretty accurate
header, err := file.Open()
if err != nil {
return attachment, false, fmt.Errorf("failed to read file header: %v", err)
}
defer header.Close()
func CacheAttachment(item models.Attachment) {
cachekit.Set[models.Attachment](
gap.Ca,
KgAttachmentCache(item.Rid),
item,
60*time.Minute,
)
}
fileHeader := make([]byte, 512)
_, err = header.Read(fileHeader)
if err != nil {
return attachment, false, err
}
attachment.MimeType = http.DetectContentType(fileHeader)
func NewAttachmentMetadata(tx *gorm.DB, user *sec.UserInfo, file *multipart.FileHeader, attachment models.Attachment) (models.Attachment, error) {
attachment.Uuid = uuid.NewString()
attachment.Rid = RandString(16)
attachment.Size = file.Size
attachment.Name = file.Filename
attachment.AccountID = user.ID
// If the user didn't provide file mimetype manually, we have to detect it
if len(attachment.MimeType) == 0 {
if ext := filepath.Ext(attachment.Name); len(ext) > 0 {
// Detect mimetype by file extensions
attachment.MimeType = mime.TypeByExtension(ext)
} else {
// Detect mimetype by file header
// This method as a fallback method, because this isn't pretty accurate
header, err := file.Open()
if err != nil {
return attachment, fmt.Errorf("failed to read file header: %v", err)
}
defer header.Close()
fileHeader := make([]byte, 512)
_, err = header.Read(fileHeader)
if err != nil {
return attachment, err
}
attachment.MimeType = http.DetectContentType(fileHeader)
}
}
if err := tx.Save(&attachment).Error; err != nil {
return attachment, linked, fmt.Errorf("failed to save attachment record: %v", err)
} else {
if len(metadataCache) > metadataCacheLimit {
clear(metadataCache)
}
metadataCache[attachment.ID] = attachment
return attachment, fmt.Errorf("failed to save attachment record: %v", err)
}
return attachment, linked, nil
return attachment, nil
}
func NewRefURLAttachment(tx *gorm.DB, user *sec.UserInfo, attachment models.Attachment) (models.Attachment, error) {
if attachment.RefURL == nil {
return attachment, fmt.Errorf("attachment doesn't have a ref url")
}
attachment.Uuid = uuid.NewString()
attachment.Rid = RandString(16)
attachment.Size = 0
attachment.Destination = models.AttachmentDstExternal
attachment.Type = models.AttachmentTypeNormal
attachment.AccountID = user.ID
if err := tx.Save(&attachment).Error; err != nil {
return attachment, fmt.Errorf("failed to save attachment record: %v", err)
}
return attachment, nil
}
func TryLinkAttachment(tx *gorm.DB, og models.Attachment, hash string) (bool, error) {
prev, err := GetAttachmentByHash(hash)
if err != nil {
return false, err
}
if prev.PoolID != nil && og.PoolID != nil && prev.PoolID != og.PoolID && prev.Pool != nil && og.Pool != nil {
if !prev.Pool.Config.Data().AllowCrossPoolEgress || !og.Pool.Config.Data().AllowCrossPoolIngress {
// Pool config doesn't allow reference
return false, nil
}
}
if err := tx.Model(&og).Updates(&models.Attachment{
RefID: &prev.ID,
Uuid: prev.Uuid,
Destination: prev.Destination,
IsSelfRef: og.AccountID == prev.AccountID,
}).Error; err != nil {
tx.Rollback()
return true, err
} else if err = tx.Model(&prev).Update("ref_count", prev.RefCount+1).Error; err != nil {
tx.Rollback()
return true, err
}
return true, nil
}
func UpdateAttachment(item models.Attachment) (models.Attachment, error) {
if err := database.C.Save(&item).Error; err != nil {
return item, err
} else {
if len(metadataCache) > metadataCacheLimit {
clear(metadataCache)
}
metadataCache[item.ID] = item
}
return item, nil
}
func DeleteAttachment(item models.Attachment) error {
var dupeCount int64
if err := database.C.
Where(&models.Attachment{HashCode: item.HashCode}).
Model(&models.Attachment{}).
Count(&dupeCount).Error; err != nil {
dupeCount = -1
func DeleteAttachment(item models.Attachment, txs ...*gorm.DB) error {
dat := item
var tx *gorm.DB
if len(txs) == 0 {
tx = database.C.Begin()
} else {
tx = txs[0]
}
if item.RefID != nil {
var refTarget models.Attachment
if err := database.C.Where("id = ?", *item.RefID).First(&refTarget).Error; err == nil {
refTarget.RefCount--
if err := tx.Save(&refTarget).Error; err != nil {
tx.Rollback()
return fmt.Errorf("unable to update ref count: %v", err)
}
}
}
if item.Thumbnail != nil {
if err := DeleteAttachment(*item.Thumbnail, tx); err != nil {
return err
}
}
if item.Compressed != nil {
if err := DeleteAttachment(*item.Compressed, tx); err != nil {
return err
}
}
if err := database.C.Delete(&item).Error; err != nil {
tx.Rollback()
return err
} else {
delete(metadataCache, item.ID)
cachekit.Delete(gap.Ca, KgAttachmentCache(item.Rid))
}
if dupeCount != -1 && dupeCount <= 1 {
return DeleteFile(item)
tx.Commit()
if dat.RefCount == 0 {
go fs.DeleteFile(dat)
}
return nil
}
func DeleteAttachmentInBatch(items []models.Attachment, txs ...*gorm.DB) error {
if len(items) == 0 {
return nil
}
var tx *gorm.DB
if len(txs) == 0 {
tx = database.C.Begin()
} else {
tx = txs[0]
}
refIDs := []uint{}
for _, item := range items {
if item.RefID != nil {
refIDs = append(refIDs, *item.RefID)
}
}
if len(refIDs) > 0 {
var refTargets []models.Attachment
if err := tx.Where("id IN ?", refIDs).Find(&refTargets).Error; err == nil {
for i := range refTargets {
refTargets[i].RefCount--
}
if err := tx.Save(&refTargets).Error; err != nil {
tx.Rollback()
return fmt.Errorf("unable to update ref count: %v", err)
}
}
}
var subAttachments []models.Attachment
for _, item := range items {
if item.Thumbnail != nil {
subAttachments = append(subAttachments, *item.Thumbnail)
}
if item.Compressed != nil {
subAttachments = append(subAttachments, *item.Compressed)
}
}
if len(subAttachments) > 0 {
if err := DeleteAttachmentInBatch(subAttachments, tx); err != nil {
tx.Rollback()
return err
}
}
rids := make([]string, len(items))
for i, item := range items {
rids[i] = item.Rid
}
if err := tx.Where("rid IN ?", rids).Delete(&models.Attachment{}).Error; err != nil {
tx.Rollback()
return err
}
for _, rid := range rids {
cachekit.Delete(gap.Ca, KgAttachmentCache(rid))
}
tx.Commit()
go func() {
for _, item := range items {
if item.RefCount == 0 {
fs.DeleteFile(item)
}
}
}()
return nil
}
func CountAttachmentUsage(tx *gorm.DB, delta int) (int64, error) {
if tx := tx.Model(&models.Attachment{}).
Update("used_count", gorm.Expr("used_count + ?", delta)); tx.Error != nil {
return tx.RowsAffected, tx.Error
} else {
return tx.RowsAffected, nil
}
}

View File

@@ -1,51 +0,0 @@
package services
import (
"errors"
"fmt"
"git.solsynth.dev/hydrogen/dealer/pkg/proto"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/models"
"gorm.io/gorm"
"reflect"
)
func LinkAccount(userinfo *proto.UserInfo) (models.Account, error) {
var account models.Account
if userinfo == nil {
return account, fmt.Errorf("remote userinfo was not found")
}
if err := database.C.Where(&models.Account{
ExternalID: uint(userinfo.Id),
}).First(&account).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
account = models.Account{
Name: userinfo.Name,
Nick: userinfo.Nick,
Avatar: userinfo.Avatar,
Banner: userinfo.Banner,
Description: userinfo.GetDescription(),
EmailAddress: userinfo.Email,
PowerLevel: 0,
ExternalID: uint(userinfo.Id),
}
return account, database.C.Save(&account).Error
}
return account, err
}
prev := account
account.Name = userinfo.Name
account.Nick = userinfo.Nick
account.Avatar = userinfo.Avatar
account.Banner = userinfo.Banner
account.Description = userinfo.GetDescription()
account.EmailAddress = userinfo.Email
var err error
if !reflect.DeepEqual(prev, account) {
err = database.C.Save(&account).Error
}
return account, err
}

View File

@@ -0,0 +1,150 @@
package services
import (
"encoding/json"
"fmt"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/fs"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/spf13/cast"
"github.com/spf13/viper"
)
func CountBoostByUser(userId uint) (int64, error) {
var count int64
if err := database.C.
Model(&models.AttachmentBoost{}).
Where("account_id = ?", userId).
Count(&count).Error; err != nil {
return count, err
}
return count, nil
}
func ListBoostByUser(userId uint, take, offset int) ([]models.AttachmentBoost, error) {
var boosts []models.AttachmentBoost
if err := database.C.
Where("account_id = ?", userId).
Limit(take).Offset(offset).
Find(&boosts).Error; err != nil {
return boosts, err
}
return boosts, nil
}
func ListBoostByAttachment(attachmentId uint) ([]models.AttachmentBoost, error) {
var boosts []models.AttachmentBoost
if err := database.C.Where("attachment_id = ?", attachmentId).Find(&boosts).Error; err != nil {
return boosts, err
}
return boosts, nil
}
func ListBoostByAttachmentWithStatus(attachmentId uint, status int) ([]models.AttachmentBoost, error) {
var boosts []models.AttachmentBoost
if err := database.C.
Where("attachment_id = ? AND status = ?", attachmentId, status).
Find(&boosts).Error; err != nil {
return boosts, err
}
return boosts, nil
}
func GetBoostByID(id uint) (models.AttachmentBoost, error) {
var boost models.AttachmentBoost
if err := database.C.
Where("id = ?", id).
Preload("Attachment").
First(&boost).Error; err != nil {
return boost, err
}
return boost, nil
}
func CreateBoost(user *sec.UserInfo, source models.Attachment, destination int) (models.AttachmentBoost, error) {
var boost models.AttachmentBoost
if err := database.C.Where("attachment_id = ? AND destination = ?", source.ID, destination).First(&boost); err == nil {
return boost, fmt.Errorf("boost already exists")
}
boost = models.AttachmentBoost{
Status: models.BoostStatusPending,
Destination: destination,
AttachmentID: source.ID,
Attachment: source,
AccountID: user.ID,
}
if des, ok := DestinationsByIndex[destination]; !ok {
return boost, fmt.Errorf("invalid destination: %d", destination)
} else {
var destBase models.BaseDestination
json.Unmarshal(des.Raw, &destBase)
if !destBase.IsBoost {
return boost, fmt.Errorf("invalid destination: %d; wasn't available for boost", destination)
}
}
if err := database.C.Save(&boost).Error; err != nil {
return boost, err
}
boost.Attachment = source
go ActivateBoost(boost)
return boost, nil
}
func ActivateBoost(boost models.AttachmentBoost) error {
log.Debug().Any("boost", boost).Msg("Activating boost...")
dests := cast.ToSlice(viper.Get("destinations"))
if boost.Destination >= len(dests) {
log.Warn().Any("boost", boost).Msg("Unable to activate boost, invalid destination...")
database.C.Model(&boost).Update("status", models.BoostStatusError)
return fmt.Errorf("invalid destination: %d", boost.Destination)
}
if err := ReUploadFile(boost.Attachment, boost.Destination, true); err != nil {
log.Warn().Any("boost", boost).Err(err).Msg("Unable to activate boost...")
database.C.Model(&boost).Update("status", models.BoostStatusError)
return err
}
log.Info().Any("boost", boost).Msg("Boost was activated successfully.")
database.C.Model(&boost).Update("status", models.BoostStatusActive)
return nil
}
func UpdateBoostStatus(boost models.AttachmentBoost, status int) (models.AttachmentBoost, error) {
if status != models.BoostStatusActive && status != models.BoostStatusSuspended {
return boost, fmt.Errorf("invalid status: %d", status)
}
err := database.C.Save(&boost).Error
return boost, err
}
func DeleteBoost(boost models.AttachmentBoost) error {
destMap := viper.GetStringMap(fmt.Sprintf("destinations.%d", boost.Destination))
var dest models.BaseDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
switch dest.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return fs.DeleteFileFromLocal(destConfigured, boost.Attachment.Uuid)
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return fs.DeleteFileFromS3(destConfigured, boost.Attachment.Uuid)
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", dest.Type)
}
}

View File

@@ -1,24 +1,28 @@
package services
import (
database2 "git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"time"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"github.com/rs/zerolog/log"
)
func DoAutoDatabaseCleanup() {
deadline := time.Now().Add(60 * time.Minute)
log.Debug().Time("deadline", deadline).Msg("Now cleaning up entire database...")
func DoUnusedAttachmentCleanup() {
deadline := time.Now().Add(-60 * time.Minute)
var count int64
for _, model := range database2.AutoMaintainRange {
tx := database2.C.Unscoped().Delete(model, "deleted_at >= ?", deadline)
if tx.Error != nil {
log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...")
}
count += tx.RowsAffected
var result []models.Attachment
if err := database.C.Where("created_at < ? AND used_count = 0", deadline).
Find(&result).Error; err != nil {
log.Error().Err(err).Msg("An error occurred when getting unused attachments...")
return
}
log.Debug().Int64("affected", count).Msg("Clean up entire database accomplished.")
if err := DeleteAttachmentInBatch(result); err != nil {
log.Error().Err(err).Msg("An error occurred when deleting unused attachments...")
return
}
log.Info().Int("count", len(result)).Msg("Deleted unused attachments...")
}

View File

@@ -0,0 +1,43 @@
package services
import (
"fmt"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog/log"
"github.com/spf13/cast"
"github.com/spf13/viper"
)
type destinationMapping struct {
Index int
Raw []byte
}
var (
DestinationsByIndex = make(map[int]destinationMapping)
DestinationsByRegion = make(map[string]destinationMapping)
)
func BuildDestinationMapping() {
count := len(cast.ToSlice(viper.Get("destinations")))
for idx := 0; idx < count; idx++ {
destMap := viper.GetStringMap(fmt.Sprintf("destinations.%d", idx))
var parsed models.BaseDestination
raw, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(raw, &parsed)
mapping := destinationMapping{
Index: idx,
Raw: raw,
}
if len(parsed.Region) > 0 {
DestinationsByIndex[idx] = mapping
DestinationsByRegion[parsed.Region] = mapping
}
}
log.Info().Int("count", count).Msg("Destinations mapping built")
}

View File

@@ -0,0 +1,150 @@
package services
import (
"errors"
"fmt"
"math"
"mime"
"mime/multipart"
"os"
"path/filepath"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
"github.com/spf13/viper"
"gorm.io/datatypes"
"gorm.io/gorm"
)
func KgAttachmentFragmentCache(rid string) string {
return cachekit.FKey("attachment-fragment", rid)
}
func NewAttachmentFragment(tx *gorm.DB, user *sec.UserInfo, fragment models.AttachmentFragment) (models.AttachmentFragment, error) {
if fragment.Fingerprint != nil {
var existsFragment models.AttachmentFragment
if err := database.C.Where(models.AttachmentFragment{
Fingerprint: fragment.Fingerprint,
AccountID: user.ID,
}).First(&existsFragment).Error; err == nil {
return existsFragment, nil
}
}
fragment.Uuid = uuid.NewString()
fragment.Rid = RandString(16)
fragment.FileChunks = datatypes.JSONMap{}
fragment.AccountID = user.ID
chunkSize := viper.GetInt64("performance.file_chunk_size")
chunkCount := math.Ceil(float64(fragment.Size) / float64(chunkSize))
for idx := 0; idx < int(chunkCount); idx++ {
cid := RandString(8)
fragment.FileChunks[cid] = idx
}
// If the user didn't provide file mimetype manually, we have to detect it
if len(fragment.MimeType) == 0 {
if ext := filepath.Ext(fragment.Name); len(ext) > 0 {
// Detect mimetype by file extensions
fragment.MimeType = mime.TypeByExtension(ext)
}
}
if err := tx.Save(&fragment).Error; err != nil {
return fragment, fmt.Errorf("failed to save attachment record: %v", err)
}
return fragment, nil
}
func GetFragmentByRID(rid string) (models.AttachmentFragment, error) {
if val, err := cachekit.Get[models.AttachmentFragment](
gap.Ca,
KgAttachmentFragmentCache(rid),
); err == nil {
return val, nil
}
var attachment models.AttachmentFragment
if err := database.C.Where(models.AttachmentFragment{
Rid: rid,
}).Preload("Pool").First(&attachment).Error; err != nil {
return attachment, err
} else {
CacheAttachmentFragment(attachment)
}
return attachment, nil
}
func CacheAttachmentFragment(item models.AttachmentFragment) {
cachekit.Set[models.AttachmentFragment](
gap.Ca,
KgAttachmentFragmentCache(item.Rid),
item,
60*time.Minute,
)
}
func UploadFragmentChunk(ctx *fiber.Ctx, cid string, file *multipart.FileHeader, meta models.AttachmentFragment) error {
destMap := viper.GetStringMap("destinations.0")
var dest models.LocalDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
tempPath := filepath.Join(dest.Path, fmt.Sprintf("%s.part%s.partial", meta.Uuid, cid))
destPath := filepath.Join(dest.Path, fmt.Sprintf("%s.part%s", meta.Uuid, cid))
if err := ctx.SaveFile(file, tempPath); err != nil {
return err
}
return os.Rename(tempPath, destPath)
}
func UploadFragmentChunkBytes(ctx *fiber.Ctx, cid string, raw []byte, meta models.AttachmentFragment) error {
destMap := viper.GetStringMap("destinations.0")
var dest models.LocalDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
tempPath := filepath.Join(dest.Path, fmt.Sprintf("%s.part%s.partial", meta.Uuid, cid))
destPath := filepath.Join(dest.Path, fmt.Sprintf("%s.part%s", meta.Uuid, cid))
if err := os.WriteFile(tempPath, raw, 0644); err != nil {
return err
}
return os.Rename(tempPath, destPath)
}
func CheckFragmentChunkExists(meta models.AttachmentFragment, cid string) bool {
destMap := viper.GetStringMap("destinations.0")
var dest models.LocalDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
path := filepath.Join(dest.Path, fmt.Sprintf("%s.part%s", meta.Uuid, cid))
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return false
} else {
return true
}
}
func FindFragmentMissingChunks(meta models.AttachmentFragment) []string {
var missing []string
for cid := range meta.FileChunks {
if !CheckFragmentChunkExists(meta, cid) {
missing = append(missing, cid)
}
}
return missing
}

View File

@@ -0,0 +1,180 @@
package services
import (
"context"
"encoding/json"
"fmt"
"math/rand/v2"
nurl "net/url"
"path/filepath"
"strings"
"time"
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio-go/v7"
"github.com/samber/lo"
"github.com/spf13/viper"
)
type openAttachmentResult struct {
Attachment models.Attachment `json:"attachment"`
Boosts []models.AttachmentBoost `json:"boost"`
}
func KgAttachmentOpenCache(rid string) string {
return fmt.Sprintf("attachment-open#%s", rid)
}
func OpenAttachmentByRID(rid string, preferredSize int, region ...string) (url string, filesize int64, mimetype string, err error) {
var result *openAttachmentResult
if val, err := cachekit.Get[openAttachmentResult](
gap.Ca,
KgAttachmentOpenCache(rid),
); err == nil {
result = &val
}
if result == nil {
var attachment models.Attachment
if err = database.C.Where(models.Attachment{
Rid: rid,
}).
Preload("Pool").
Preload("Thumbnail").
Preload("Compressed").
First(&attachment).Error; err != nil {
return
}
var boosts []models.AttachmentBoost
boosts, err = ListBoostByAttachmentWithStatus(attachment.ID, models.BoostStatusActive)
if err != nil {
return
}
result = &openAttachmentResult{
Attachment: attachment,
Boosts: boosts,
}
}
if len(result.Attachment.MimeType) > 0 {
mimetype = result.Attachment.MimeType
}
if result.Attachment.RefURL != nil {
url = *result.Attachment.RefURL
filesize = 0
return
}
filesize = result.Attachment.Size
var dest models.BaseDestination
var rawDest []byte
if len(region) > 0 {
if des, ok := DestinationsByRegion[region[0]]; ok {
for _, boost := range result.Boosts {
if boost.Destination == des.Index {
rawDest = des.Raw
json.Unmarshal(rawDest, &dest)
}
}
}
}
if rawDest == nil {
if len(result.Boosts) > 0 {
randomIdx := rand.IntN(len(result.Boosts))
boost := result.Boosts[randomIdx]
if des, ok := DestinationsByIndex[boost.Destination]; ok {
rawDest = des.Raw
json.Unmarshal(rawDest, &dest)
}
} else {
if des, ok := DestinationsByIndex[result.Attachment.Destination]; ok {
rawDest = des.Raw
json.Unmarshal(rawDest, &dest)
}
}
}
if rawDest == nil {
err = fmt.Errorf("no destination found")
return
}
switch dest.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
url = "file://" + filepath.Join(destConfigured.Path, result.Attachment.Uuid)
return
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
if len(destConfigured.AccessBaseURL) > 0 {
url = fmt.Sprintf(
"%s/%s",
destConfigured.AccessBaseURL,
nurl.QueryEscape(filepath.Join(destConfigured.Path, result.Attachment.Uuid)),
)
} else if destConfigured.EnableSigned {
var client *minio.Client
client, err = destConfigured.GetClient()
if err != nil {
return
}
var uri *nurl.URL
uri, err = client.PresignedGetObject(context.Background(), destConfigured.Bucket, result.Attachment.Uuid, 60*time.Minute, nil)
if err != nil {
return
}
url = uri.String()
} else {
protocol := lo.Ternary(destConfigured.EnableSSL, "https", "http")
url = fmt.Sprintf(
"%s://%s.%s/%s",
protocol,
destConfigured.Bucket,
destConfigured.Endpoint,
nurl.QueryEscape(filepath.Join(destConfigured.Path, result.Attachment.Uuid)),
)
}
if strings.HasPrefix(mimetype, "image") && filesize >= viper.GetInt64("traffic.minimum_size") {
if len(destConfigured.ImageProxyURL) > 0 && preferredSize > 0 {
url = fmt.Sprintf(
"%s/%dx%d,fit/%s",
destConfigured.ImageProxyURL,
preferredSize,
preferredSize,
url,
)
filesize = int64(preferredSize * preferredSize)
}
}
return
default:
err = fmt.Errorf("invalid destination: unsupported protocol %s", dest.Type)
return
}
}
func CacheOpenAttachment(item *openAttachmentResult) {
if item == nil {
return
}
cachekit.Set[openAttachmentResult](
gap.Ca,
KgAttachmentCache(item.Attachment.Rid),
*item,
60*time.Minute,
)
}

View File

@@ -0,0 +1,77 @@
package services
import (
"context"
"fmt"
"time"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
wproto "git.solsynth.dev/hypernet/wallet/pkg/proto"
"github.com/rs/zerolog/log"
"github.com/samber/lo"
"github.com/spf13/viper"
)
func GetLastDayUploadedBytes(user uint) (int64, error) {
deadline := time.Now().Add(-24 * time.Hour)
var totalSize int64
if err := database.C.
Model(&models.Attachment{}).
Where("account_id = ?", user).
Where("created_at >= ?", deadline).
Select("SUM(size)").
Scan(&totalSize).Error; err != nil {
return totalSize, err
}
return totalSize, nil
}
// PlaceOrder create a transaction if needed for user
// Pricing according here: https://kb.solsynth.dev/solar-network/wallet#file-uploads
func PlaceOrder(user uint, filesize int64, withDiscount bool) error {
currentBytes, _ := GetLastDayUploadedBytes(user)
discountFileSize := viper.GetInt64("payment.discount")
if currentBytes+filesize <= discountFileSize {
// Discount included
return nil
}
var amount float64
if withDiscount {
amount = float64(filesize) / 1024 / 1024 * 1
} else {
amount = float64(filesize) / 1024 / 1024 * 1
}
if !withDiscount {
amount += 10 // Service fee
}
conn, err := gap.Nx.GetClientGrpcConn("wa")
if err != nil {
return fmt.Errorf("unable to connect wallet: %v", err)
}
wc := wproto.NewPaymentServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
resp, err := wc.MakeTransactionWithAccount(ctx, &wproto.MakeTransactionWithAccountRequest{
PayerAccountId: lo.ToPtr(uint64(user)),
Amount: amount,
Remark: "File Uploading Fee",
Currency: "normal",
})
if err != nil {
return err
}
log.Info().
Uint64("transaction", resp.Id).Float64("amount", amount).Bool("discount", withDiscount).
Msg("Order placed for charge file uploading fee...")
return nil
}

View File

@@ -0,0 +1,59 @@
package services
import (
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
)
func ListAttachmentPool() ([]models.AttachmentPool, error) {
var pools []models.AttachmentPool
if err := database.C.Find(&pools).Error; err != nil {
return pools, err
}
return pools, nil
}
func GetAttachmentPool(id uint) (models.AttachmentPool, error) {
var pool models.AttachmentPool
if err := database.C.Where("id = ?", id).First(&pool).Error; err != nil {
return pool, err
}
return pool, nil
}
func GetAttachmentPoolByAlias(alias string) (models.AttachmentPool, error) {
var pool models.AttachmentPool
if err := database.C.Where("alias = ?", alias).First(&pool).Error; err != nil {
return pool, err
}
return pool, nil
}
func GetAttachmentPoolWithUser(id uint, userId uint) (models.AttachmentPool, error) {
var pool models.AttachmentPool
if err := database.C.Where("id = ? AND account_id = ?", id, userId).First(&pool).Error; err != nil {
return pool, err
}
return pool, nil
}
func NewAttachmentPool(pool models.AttachmentPool) (models.AttachmentPool, error) {
if err := database.C.Save(&pool).Error; err != nil {
return pool, err
}
return pool, nil
}
func UpdateAttachmentPool(pool models.AttachmentPool) (models.AttachmentPool, error) {
if err := database.C.Save(&pool).Error; err != nil {
return pool, err
}
return pool, nil
}
func DeleteAttachmentPool(pool models.AttachmentPool) (models.AttachmentPool, error) {
if err := database.C.Delete(&pool).Error; err != nil {
return pool, err
}
return pool, nil
}

View File

@@ -0,0 +1,15 @@
package services
import (
"math/rand"
)
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
func RandString(length int) string {
builder := make([]rune, length)
for i := range builder {
builder[i] = letters[rand.Intn(len(letters))]
}
return string(builder)
}

View File

@@ -1,61 +0,0 @@
package services
import (
"context"
"fmt"
"os"
"path/filepath"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/models"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/spf13/viper"
)
func DeleteFile(meta models.Attachment) error {
destMap := viper.GetStringMap("destinations")
dest, destOk := destMap[meta.Destination]
if !destOk {
return fmt.Errorf("invalid destination: destination configuration was not found")
}
var destParsed models.BaseDestination
rawDest, _ := jsoniter.Marshal(dest)
_ = jsoniter.Unmarshal(rawDest, &destParsed)
switch destParsed.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return DeleteFileFromLocal(destConfigured, meta)
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return DeleteFileFromS3(destConfigured, meta)
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", destParsed.Type)
}
}
func DeleteFileFromLocal(config models.LocalDestination, meta models.Attachment) error {
fullpath := filepath.Join(config.Path, meta.Uuid)
return os.Remove(fullpath)
}
func DeleteFileFromS3(config models.S3Destination, meta models.Attachment) error {
client, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.SecretID, config.SecretKey, ""),
Secure: config.EnableSSL,
})
if err != nil {
return fmt.Errorf("unable to configure s3 client: %v", err)
}
err = client.RemoveObject(context.Background(), config.Bucket, filepath.Join(config.Path, meta.Uuid), minio.RemoveObjectOptions{})
if err != nil {
return fmt.Errorf("unable to upload file to s3: %v", err)
}
return nil
}

View File

@@ -0,0 +1,53 @@
package services
import (
"fmt"
"strings"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
)
func SetAttachmentAsThumbnail(item models.Attachment) (models.Attachment, error) {
if !strings.HasPrefix(item.MimeType, "image") {
return item, fmt.Errorf("thumbnail must be an image")
}
item.Type = models.AttachmentTypeThumbnail
item.UsedCount++
if err := database.C.Save(&item).Error; err != nil {
return item, err
}
return item, nil
}
func SetAttachmentAsCompressed(item models.Attachment) (models.Attachment, error) {
item.Type = models.AttachmentTypeCompressed
item.UsedCount++
if err := database.C.Save(&item).Error; err != nil {
return item, err
}
return item, nil
}
func UnsetAttachmentAsThumbnail(item models.Attachment) (models.Attachment, error) {
item.Type = models.AttachmentTypeNormal
item.UsedCount--
if err := database.C.Save(&item).Error; err != nil {
return item, err
}
return item, nil
}
func UnsetAttachmentAsCompressed(item models.Attachment) (models.Attachment, error) {
item.Type = models.AttachmentTypeNormal
item.UsedCount--
if err := database.C.Save(&item).Error; err != nil {
return item, err
}
return item, nil
}

View File

@@ -0,0 +1,60 @@
package services
import (
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"gorm.io/gorm"
)
func GetStickerPack(id uint) (models.StickerPack, error) {
var pack models.StickerPack
if err := database.C.Where("id = ?", id).First(&pack).Error; err != nil {
return pack, err
}
return pack, nil
}
func GetStickerPackWithUser(id, userId uint) (models.StickerPack, error) {
var pack models.StickerPack
if err := database.C.Where("id = ? AND account_id = ?", id, userId).First(&pack).Error; err != nil {
return pack, err
}
return pack, nil
}
func ListStickerPackWithStickers(tx *gorm.DB, take, offset int) ([]models.StickerPack, error) {
var packs []models.StickerPack
if err := tx.Limit(take).Offset(offset).Preload("Stickers").Preload("Stickers.Attachment").Find(&packs).Error; err != nil {
return packs, err
}
return packs, nil
}
func NewStickerPack(user *sec.UserInfo, prefix, name, desc string) (models.StickerPack, error) {
pack := models.StickerPack{
Prefix: prefix,
Name: name,
Description: desc,
AccountID: user.ID,
}
if err := database.C.Save(&pack).Error; err != nil {
return pack, err
}
return pack, nil
}
func UpdateStickerPack(pack models.StickerPack) (models.StickerPack, error) {
if err := database.C.Save(&pack).Error; err != nil {
return pack, err
}
return pack, nil
}
func DeleteStickerPack(pack models.StickerPack) (models.StickerPack, error) {
if err := database.C.Delete(&pack).Error; err != nil {
return pack, err
}
return pack, nil
}

View File

@@ -0,0 +1,109 @@
package services
import (
"errors"
"fmt"
"gorm.io/gorm"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"github.com/spf13/viper"
)
func GetStickerLikeAlias(alias string) ([]models.Sticker, error) {
var stickers []models.Sticker
prefix := viper.GetString("database.prefix")
if err := database.C.
Joins(fmt.Sprintf("LEFT JOIN %ssticker_packs pk ON pack_id = pk.id", prefix)).
Where("UPPER(CONCAT(pk.prefix, alias)) LIKE UPPER(?)", "%"+alias+"%").
Preload("Attachment").Preload("Pack").
Limit(10).
Find(&stickers).Error; err != nil {
return stickers, err
}
return stickers, nil
}
func GetStickerWithAlias(alias string) (models.Sticker, error) {
var sticker models.Sticker
prefix := viper.GetString("database.prefix")
if err := database.C.
Joins(fmt.Sprintf("LEFT JOIN %ssticker_packs pk ON pack_id = pk.id", prefix)).
Where("UPPER(CONCAT(pk.prefix, alias)) = UPPER(?)", alias).
Preload("Attachment").Preload("Pack").
First(&sticker).Error; err != nil {
return sticker, err
}
return sticker, nil
}
func GetSticker(id uint) (models.Sticker, error) {
var sticker models.Sticker
if err := database.C.Where("id = ?", id).Preload("Attachment").First(&sticker).Error; err != nil {
return sticker, err
}
return sticker, nil
}
func GetStickerWithUser(id, userId uint) (models.Sticker, error) {
var sticker models.Sticker
if err := database.C.Where("id = ? AND account_id = ?", id, userId).First(&sticker).Error; err != nil {
return sticker, err
}
return sticker, nil
}
func NewSticker(sticker models.Sticker) (models.Sticker, error) {
if err := database.C.Save(&sticker).Error; err != nil {
return sticker, err
}
return sticker, nil
}
func UpdateSticker(sticker models.Sticker) (models.Sticker, error) {
if err := database.C.Save(&sticker).Error; err != nil {
return sticker, err
}
return sticker, nil
}
func DeleteSticker(sticker models.Sticker) (models.Sticker, error) {
if err := database.C.Delete(&sticker).Error; err != nil {
return sticker, err
}
return sticker, nil
}
func AddStickerPack(user uint, pack models.StickerPack) (models.StickerPackOwnership, error) {
var ownership models.StickerPackOwnership
if err := database.C.
Where("account_id = ? AND pack_id = ?", user, pack.ID).
First(&ownership).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return ownership, fmt.Errorf("unable to get current ownership: %v", err)
} else if err == nil {
return ownership, fmt.Errorf("you already own this pack")
}
ownership = models.StickerPackOwnership{
AccountID: user,
PackID: pack.ID,
}
err := database.C.Save(&ownership).Error
return ownership, err
}
func RemoveStickerPack(user uint, pack models.StickerPack) (models.StickerPackOwnership, error) {
var ownership models.StickerPackOwnership
if err := database.C.
Where("account_id = ? AND pack_id = ?", user, pack.ID).
First(&ownership).Error; err != nil {
return ownership, fmt.Errorf("unable to get current ownership: %v", err)
}
err := database.C.Delete(&ownership).Error
return ownership, err
}

View File

@@ -1,76 +1,115 @@
package services
import (
"bytes"
"context"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/models"
"git.solsynth.dev/hypernet/paperclip/pkg/filekit/models"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/fs"
"github.com/gofiber/fiber/v2"
jsoniter "github.com/json-iterator/go"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/spf13/viper"
)
func UploadFile(destName string, ctx *fiber.Ctx, file *multipart.FileHeader, meta models.Attachment) error {
destMap := viper.GetStringMap("destinations")
dest, destOk := destMap[destName]
if !destOk {
return fmt.Errorf("invalid destination: destination configuration was not found")
}
func UploadFileToTemporary(ctx *fiber.Ctx, file *multipart.FileHeader, meta models.Attachment) error {
destMap := viper.GetStringMap(fmt.Sprintf("destinations.%d", meta.Destination))
var destParsed models.BaseDestination
rawDest, _ := jsoniter.Marshal(dest)
_ = jsoniter.Unmarshal(rawDest, &destParsed)
var dest models.BaseDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
switch destParsed.Type {
switch dest.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return UploadFileToLocal(destConfigured, ctx, file, meta)
return ctx.SaveFile(file, filepath.Join(destConfigured.Path, meta.Uuid))
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", dest.Type)
}
}
func ReUploadFile(meta models.Attachment, dst int, doNotUpdate ...bool) error {
if meta.Destination == dst {
return fmt.Errorf("destnation cannot be reversed temporary or the same as the original")
}
prevDst := meta.Destination
inDst, err := fs.DownloadFileToLocal(meta, prevDst)
if err != nil {
return fmt.Errorf("unable to retrieve file content: %v", err)
}
cleanupDst := func() {
if len(doNotUpdate) == 0 || !doNotUpdate[0] {
database.C.Model(&meta).Update("destination", dst)
}
if prevDst == models.AttachmentDstTemporary {
return
}
os.Remove(inDst)
}
meta.Destination = dst
destMap := viper.GetStringMap(fmt.Sprintf("destinations.%d", dst))
var dest models.BaseDestination
rawDest, _ := jsoniter.Marshal(destMap)
_ = jsoniter.Unmarshal(rawDest, &dest)
switch dest.Type {
case models.DestinationTypeLocal:
var destConfigured models.LocalDestination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
in, err := os.Open(inDst)
if err != nil {
return fmt.Errorf("unable to open file in temporary storage: %v", err)
}
defer in.Close()
out, err := os.Create(filepath.Join(destConfigured.Path, meta.Uuid))
if err != nil {
return fmt.Errorf("unable to open dest file: %v", err)
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return fmt.Errorf("unable to copy data to dest file: %v", err)
}
cleanupDst()
return nil
case models.DestinationTypeS3:
var destConfigured models.S3Destination
_ = jsoniter.Unmarshal(rawDest, &destConfigured)
return UploadFileToS3(destConfigured, file, meta)
client, err := destConfigured.GetClient()
if err != nil {
return fmt.Errorf("unable to configure s3 client: %v", err)
}
_, err = client.FPutObject(context.Background(), destConfigured.Bucket, filepath.Join(destConfigured.Path, meta.Uuid), inDst, minio.PutObjectOptions{
ContentType: meta.MimeType,
SendContentMd5: false,
DisableContentSha256: true,
PartSize: 10 * 1024 * 1024,
ConcurrentStreamParts: true,
NumThreads: 4,
})
if err != nil {
return fmt.Errorf("unable to upload file to s3: %v", err)
}
cleanupDst()
return nil
default:
return fmt.Errorf("invalid destination: unsupported protocol %s", destParsed.Type)
return fmt.Errorf("invalid destination: unsupported protocol %s", dest.Type)
}
}
func UploadFileToLocal(config models.LocalDestination, ctx *fiber.Ctx, file *multipart.FileHeader, meta models.Attachment) error {
return ctx.SaveFile(file, filepath.Join(config.Path, meta.Uuid))
}
func UploadFileToS3(config models.S3Destination, file *multipart.FileHeader, meta models.Attachment) error {
header, err := file.Open()
if err != nil {
return fmt.Errorf("read upload file: %v", err)
}
defer header.Close()
buffer := bytes.NewBuffer(nil)
if _, err := io.Copy(buffer, header); err != nil {
return fmt.Errorf("create io reader for upload file: %v", err)
}
client, err := minio.New(config.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(config.SecretID, config.SecretKey, ""),
Secure: config.EnableSSL,
})
if err != nil {
return fmt.Errorf("unable to configure s3 client: %v", err)
}
_, err = client.PutObject(context.Background(), config.Bucket, filepath.Join(config.Path, meta.Uuid), buffer, -1, minio.PutObjectOptions{
ContentType: meta.MimeType,
})
if err != nil {
return fmt.Errorf("unable to upload file to s3: %v", err)
}
return nil
}

View File

@@ -1,19 +1,24 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/database"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/gap"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/grpc"
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
pkg "git.solsynth.dev/hypernet/paperclip/pkg/internal"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/fs"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/gap"
"github.com/fatih/color"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/server"
"git.solsynth.dev/hydrogen/paperclip/pkg/internal/services"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/database"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/grpc"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/server"
"git.solsynth.dev/hypernet/paperclip/pkg/internal/services"
"github.com/robfig/cron/v3"
pkg "git.solsynth.dev/hydrogen/paperclip/pkg/internal"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
@@ -25,6 +30,12 @@ func init() {
}
func main() {
// Booting screen
fmt.Println(color.YellowString(" ____ _ _\n| _ \\ __ _ _ __ ___ _ __ ___| (_)_ __\n| |_) / _` | '_ \\ / _ \\ '__/ __| | | '_ \\\n| __/ (_| | |_) | __/ | | (__| | | |_) |\n|_| \\__,_| .__/ \\___|_| \\___|_|_| .__/\n |_| |_|"))
fmt.Printf("%s v%s\n", color.New(color.FgHiYellow).Add(color.Bold).Sprintf("Hypernet.Paperclip"), pkg.AppVersion)
fmt.Printf("The upload service in Hypernet\n")
color.HiBlack("=====================================================\n")
// Configure settings
viper.AddConfigPath(".")
viper.AddConfigPath("..")
@@ -36,39 +47,54 @@ func main() {
log.Panic().Err(err).Msg("An error occurred when loading settings.")
}
// Connect to nexus
if err := gap.InitializeToNexus(); err != nil {
log.Error().Err(err).Msg("An error occurred when registering service to nexus...")
}
// Load keypair
if reader, err := sec.NewInternalTokenReader(viper.GetString("security.internal_public_key")); err != nil {
log.Error().Err(err).Msg("An error occurred when reading internal public key for jwt. Authentication related features will be disabled.")
} else {
server.IReader = reader
log.Info().Msg("Internal jwt public key loaded.")
}
// Connect to database
if err := database.NewSource(); err != nil {
if err := database.NewGorm(); err != nil {
log.Fatal().Err(err).Msg("An error occurred when connect to database.")
} else if err := database.RunMigration(database.C); err != nil {
log.Fatal().Err(err).Msg("An error occurred when running database auto migration.")
}
// Connect other services
if err := gap.RegisterService(); err != nil {
log.Error().Err(err).Msg("An error occurred when registering service to dealer...")
// Set up some workers
for idx := 0; idx < viper.GetInt("workers.files_analyze"); idx++ {
go services.StartConsumeAnalyzeTask()
}
// Configure timed tasks
quartz := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(&log.Logger)))
quartz.AddFunc("@every 60m", services.DoAutoDatabaseCleanup)
// quartz.AddFunc("@every 60m", services.DoUnusedAttachmentCleanup)
quartz.AddFunc("@every 60m", fs.RunMarkLifecycleDeletionTask)
quartz.AddFunc("@every 60m", fs.RunMarkMultipartDeletionTask)
quartz.AddFunc("@midnight", fs.RunScheduleDeletionTask)
quartz.Start()
// Server
server.NewServer()
go server.Listen()
go server.NewServer().Listen()
// Grpc Server
grpc.NewGRPC()
go grpc.ListenGRPC()
go grpc.NewGrpc().Listen()
// Post-boot actions
services.BuildDestinationMapping()
services.ScanUnanalyzedFileFromDatabase()
fs.RunMarkLifecycleDeletionTask()
// Messages
log.Info().Msgf("Paperclip v%s is started...", pkg.AppVersion)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info().Msgf("Paperclip v%s is quitting...", pkg.AppVersion)
quartz.Stop()
}

678
pkg/proto/attachment.pb.go Normal file
View File

@@ -0,0 +1,678 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v5.29.3
// source: attachment.proto
package proto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type GetAttachmentRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id *uint64 `protobuf:"varint,1,opt,name=id,proto3,oneof" json:"id,omitempty"`
Rid *string `protobuf:"bytes,2,opt,name=rid,proto3,oneof" json:"rid,omitempty"`
UserId *uint64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3,oneof" json:"user_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAttachmentRequest) Reset() {
*x = GetAttachmentRequest{}
mi := &file_attachment_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAttachmentRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAttachmentRequest) ProtoMessage() {}
func (x *GetAttachmentRequest) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetAttachmentRequest.ProtoReflect.Descriptor instead.
func (*GetAttachmentRequest) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{0}
}
func (x *GetAttachmentRequest) GetId() uint64 {
if x != nil && x.Id != nil {
return *x.Id
}
return 0
}
func (x *GetAttachmentRequest) GetRid() string {
if x != nil && x.Rid != nil {
return *x.Rid
}
return ""
}
func (x *GetAttachmentRequest) GetUserId() uint64 {
if x != nil && x.UserId != nil {
return *x.UserId
}
return 0
}
type GetAttachmentResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Attachment []byte `protobuf:"bytes,1,opt,name=attachment,proto3,oneof" json:"attachment,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetAttachmentResponse) Reset() {
*x = GetAttachmentResponse{}
mi := &file_attachment_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetAttachmentResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetAttachmentResponse) ProtoMessage() {}
func (x *GetAttachmentResponse) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetAttachmentResponse.ProtoReflect.Descriptor instead.
func (*GetAttachmentResponse) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{1}
}
func (x *GetAttachmentResponse) GetAttachment() []byte {
if x != nil {
return x.Attachment
}
return nil
}
type ListAttachmentRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id []uint64 `protobuf:"varint,1,rep,packed,name=id,proto3" json:"id,omitempty"`
Rid []string `protobuf:"bytes,2,rep,name=rid,proto3" json:"rid,omitempty"`
UserId *uint64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3,oneof" json:"user_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListAttachmentRequest) Reset() {
*x = ListAttachmentRequest{}
mi := &file_attachment_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListAttachmentRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListAttachmentRequest) ProtoMessage() {}
func (x *ListAttachmentRequest) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListAttachmentRequest.ProtoReflect.Descriptor instead.
func (*ListAttachmentRequest) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{2}
}
func (x *ListAttachmentRequest) GetId() []uint64 {
if x != nil {
return x.Id
}
return nil
}
func (x *ListAttachmentRequest) GetRid() []string {
if x != nil {
return x.Rid
}
return nil
}
func (x *ListAttachmentRequest) GetUserId() uint64 {
if x != nil && x.UserId != nil {
return *x.UserId
}
return 0
}
type ListAttachmentResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Attachments [][]byte `protobuf:"bytes,1,rep,name=attachments,proto3" json:"attachments,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListAttachmentResponse) Reset() {
*x = ListAttachmentResponse{}
mi := &file_attachment_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListAttachmentResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListAttachmentResponse) ProtoMessage() {}
func (x *ListAttachmentResponse) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListAttachmentResponse.ProtoReflect.Descriptor instead.
func (*ListAttachmentResponse) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{3}
}
func (x *ListAttachmentResponse) GetAttachments() [][]byte {
if x != nil {
return x.Attachments
}
return nil
}
type UpdateVisibilityRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id []uint64 `protobuf:"varint,1,rep,packed,name=id,proto3" json:"id,omitempty"`
Rid []string `protobuf:"bytes,2,rep,name=rid,proto3" json:"rid,omitempty"`
IsIndexable bool `protobuf:"varint,3,opt,name=is_indexable,json=isIndexable,proto3" json:"is_indexable,omitempty"`
UserId *uint64 `protobuf:"varint,4,opt,name=user_id,json=userId,proto3,oneof" json:"user_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateVisibilityRequest) Reset() {
*x = UpdateVisibilityRequest{}
mi := &file_attachment_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateVisibilityRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateVisibilityRequest) ProtoMessage() {}
func (x *UpdateVisibilityRequest) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateVisibilityRequest.ProtoReflect.Descriptor instead.
func (*UpdateVisibilityRequest) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{4}
}
func (x *UpdateVisibilityRequest) GetId() []uint64 {
if x != nil {
return x.Id
}
return nil
}
func (x *UpdateVisibilityRequest) GetRid() []string {
if x != nil {
return x.Rid
}
return nil
}
func (x *UpdateVisibilityRequest) GetIsIndexable() bool {
if x != nil {
return x.IsIndexable
}
return false
}
func (x *UpdateVisibilityRequest) GetUserId() uint64 {
if x != nil && x.UserId != nil {
return *x.UserId
}
return 0
}
type UpdateVisibilityResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Count int32 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateVisibilityResponse) Reset() {
*x = UpdateVisibilityResponse{}
mi := &file_attachment_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateVisibilityResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateVisibilityResponse) ProtoMessage() {}
func (x *UpdateVisibilityResponse) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateVisibilityResponse.ProtoReflect.Descriptor instead.
func (*UpdateVisibilityResponse) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{5}
}
func (x *UpdateVisibilityResponse) GetCount() int32 {
if x != nil {
return x.Count
}
return 0
}
type UpdateUsageRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id []uint64 `protobuf:"varint,1,rep,packed,name=id,proto3" json:"id,omitempty"`
Rid []string `protobuf:"bytes,2,rep,name=rid,proto3" json:"rid,omitempty"`
Delta int64 `protobuf:"varint,3,opt,name=delta,proto3" json:"delta,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateUsageRequest) Reset() {
*x = UpdateUsageRequest{}
mi := &file_attachment_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateUsageRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateUsageRequest) ProtoMessage() {}
func (x *UpdateUsageRequest) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateUsageRequest.ProtoReflect.Descriptor instead.
func (*UpdateUsageRequest) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{6}
}
func (x *UpdateUsageRequest) GetId() []uint64 {
if x != nil {
return x.Id
}
return nil
}
func (x *UpdateUsageRequest) GetRid() []string {
if x != nil {
return x.Rid
}
return nil
}
func (x *UpdateUsageRequest) GetDelta() int64 {
if x != nil {
return x.Delta
}
return 0
}
type UpdateUsageResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Count int32 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateUsageResponse) Reset() {
*x = UpdateUsageResponse{}
mi := &file_attachment_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateUsageResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateUsageResponse) ProtoMessage() {}
func (x *UpdateUsageResponse) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateUsageResponse.ProtoReflect.Descriptor instead.
func (*UpdateUsageResponse) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{7}
}
func (x *UpdateUsageResponse) GetCount() int32 {
if x != nil {
return x.Count
}
return 0
}
type DeleteAttachmentRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id []uint64 `protobuf:"varint,1,rep,packed,name=id,proto3" json:"id,omitempty"`
Rid []string `protobuf:"bytes,2,rep,name=rid,proto3" json:"rid,omitempty"`
UserId *uint64 `protobuf:"varint,3,opt,name=user_id,json=userId,proto3,oneof" json:"user_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteAttachmentRequest) Reset() {
*x = DeleteAttachmentRequest{}
mi := &file_attachment_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteAttachmentRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteAttachmentRequest) ProtoMessage() {}
func (x *DeleteAttachmentRequest) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[8]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteAttachmentRequest.ProtoReflect.Descriptor instead.
func (*DeleteAttachmentRequest) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{8}
}
func (x *DeleteAttachmentRequest) GetId() []uint64 {
if x != nil {
return x.Id
}
return nil
}
func (x *DeleteAttachmentRequest) GetRid() []string {
if x != nil {
return x.Rid
}
return nil
}
func (x *DeleteAttachmentRequest) GetUserId() uint64 {
if x != nil && x.UserId != nil {
return *x.UserId
}
return 0
}
type DeleteAttachmentResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Count int32 `protobuf:"varint,1,opt,name=count,proto3" json:"count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteAttachmentResponse) Reset() {
*x = DeleteAttachmentResponse{}
mi := &file_attachment_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteAttachmentResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteAttachmentResponse) ProtoMessage() {}
func (x *DeleteAttachmentResponse) ProtoReflect() protoreflect.Message {
mi := &file_attachment_proto_msgTypes[9]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteAttachmentResponse.ProtoReflect.Descriptor instead.
func (*DeleteAttachmentResponse) Descriptor() ([]byte, []int) {
return file_attachment_proto_rawDescGZIP(), []int{9}
}
func (x *DeleteAttachmentResponse) GetCount() int32 {
if x != nil {
return x.Count
}
return 0
}
var File_attachment_proto protoreflect.FileDescriptor
const file_attachment_proto_rawDesc = "" +
"\n" +
"\x10attachment.proto\x12\x05proto\"{\n" +
"\x14GetAttachmentRequest\x12\x13\n" +
"\x02id\x18\x01 \x01(\x04H\x00R\x02id\x88\x01\x01\x12\x15\n" +
"\x03rid\x18\x02 \x01(\tH\x01R\x03rid\x88\x01\x01\x12\x1c\n" +
"\auser_id\x18\x03 \x01(\x04H\x02R\x06userId\x88\x01\x01B\x05\n" +
"\x03_idB\x06\n" +
"\x04_ridB\n" +
"\n" +
"\b_user_id\"K\n" +
"\x15GetAttachmentResponse\x12#\n" +
"\n" +
"attachment\x18\x01 \x01(\fH\x00R\n" +
"attachment\x88\x01\x01B\r\n" +
"\v_attachment\"c\n" +
"\x15ListAttachmentRequest\x12\x0e\n" +
"\x02id\x18\x01 \x03(\x04R\x02id\x12\x10\n" +
"\x03rid\x18\x02 \x03(\tR\x03rid\x12\x1c\n" +
"\auser_id\x18\x03 \x01(\x04H\x00R\x06userId\x88\x01\x01B\n" +
"\n" +
"\b_user_id\":\n" +
"\x16ListAttachmentResponse\x12 \n" +
"\vattachments\x18\x01 \x03(\fR\vattachments\"\x88\x01\n" +
"\x17UpdateVisibilityRequest\x12\x0e\n" +
"\x02id\x18\x01 \x03(\x04R\x02id\x12\x10\n" +
"\x03rid\x18\x02 \x03(\tR\x03rid\x12!\n" +
"\fis_indexable\x18\x03 \x01(\bR\visIndexable\x12\x1c\n" +
"\auser_id\x18\x04 \x01(\x04H\x00R\x06userId\x88\x01\x01B\n" +
"\n" +
"\b_user_id\"0\n" +
"\x18UpdateVisibilityResponse\x12\x14\n" +
"\x05count\x18\x01 \x01(\x05R\x05count\"L\n" +
"\x12UpdateUsageRequest\x12\x0e\n" +
"\x02id\x18\x01 \x03(\x04R\x02id\x12\x10\n" +
"\x03rid\x18\x02 \x03(\tR\x03rid\x12\x14\n" +
"\x05delta\x18\x03 \x01(\x03R\x05delta\"+\n" +
"\x13UpdateUsageResponse\x12\x14\n" +
"\x05count\x18\x01 \x01(\x05R\x05count\"e\n" +
"\x17DeleteAttachmentRequest\x12\x0e\n" +
"\x02id\x18\x01 \x03(\x04R\x02id\x12\x10\n" +
"\x03rid\x18\x02 \x03(\tR\x03rid\x12\x1c\n" +
"\auser_id\x18\x03 \x01(\x04H\x00R\x06userId\x88\x01\x01B\n" +
"\n" +
"\b_user_id\"0\n" +
"\x18DeleteAttachmentResponse\x12\x14\n" +
"\x05count\x18\x01 \x01(\x05R\x05count2\xa8\x03\n" +
"\x11AttachmentService\x12L\n" +
"\rGetAttachment\x12\x1b.proto.GetAttachmentRequest\x1a\x1c.proto.GetAttachmentResponse\"\x00\x12O\n" +
"\x0eListAttachment\x12\x1c.proto.ListAttachmentRequest\x1a\x1d.proto.ListAttachmentResponse\"\x00\x12U\n" +
"\x10UpdateVisibility\x12\x1e.proto.UpdateVisibilityRequest\x1a\x1f.proto.UpdateVisibilityResponse\"\x00\x12F\n" +
"\vUpdateUsage\x12\x19.proto.UpdateUsageRequest\x1a\x1a.proto.UpdateUsageResponse\"\x00\x12U\n" +
"\x10DeleteAttachment\x12\x1e.proto.DeleteAttachmentRequest\x1a\x1f.proto.DeleteAttachmentResponse\"\x00B\tZ\a.;protob\x06proto3"
var (
file_attachment_proto_rawDescOnce sync.Once
file_attachment_proto_rawDescData []byte
)
func file_attachment_proto_rawDescGZIP() []byte {
file_attachment_proto_rawDescOnce.Do(func() {
file_attachment_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_attachment_proto_rawDesc), len(file_attachment_proto_rawDesc)))
})
return file_attachment_proto_rawDescData
}
var file_attachment_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_attachment_proto_goTypes = []any{
(*GetAttachmentRequest)(nil), // 0: proto.GetAttachmentRequest
(*GetAttachmentResponse)(nil), // 1: proto.GetAttachmentResponse
(*ListAttachmentRequest)(nil), // 2: proto.ListAttachmentRequest
(*ListAttachmentResponse)(nil), // 3: proto.ListAttachmentResponse
(*UpdateVisibilityRequest)(nil), // 4: proto.UpdateVisibilityRequest
(*UpdateVisibilityResponse)(nil), // 5: proto.UpdateVisibilityResponse
(*UpdateUsageRequest)(nil), // 6: proto.UpdateUsageRequest
(*UpdateUsageResponse)(nil), // 7: proto.UpdateUsageResponse
(*DeleteAttachmentRequest)(nil), // 8: proto.DeleteAttachmentRequest
(*DeleteAttachmentResponse)(nil), // 9: proto.DeleteAttachmentResponse
}
var file_attachment_proto_depIdxs = []int32{
0, // 0: proto.AttachmentService.GetAttachment:input_type -> proto.GetAttachmentRequest
2, // 1: proto.AttachmentService.ListAttachment:input_type -> proto.ListAttachmentRequest
4, // 2: proto.AttachmentService.UpdateVisibility:input_type -> proto.UpdateVisibilityRequest
6, // 3: proto.AttachmentService.UpdateUsage:input_type -> proto.UpdateUsageRequest
8, // 4: proto.AttachmentService.DeleteAttachment:input_type -> proto.DeleteAttachmentRequest
1, // 5: proto.AttachmentService.GetAttachment:output_type -> proto.GetAttachmentResponse
3, // 6: proto.AttachmentService.ListAttachment:output_type -> proto.ListAttachmentResponse
5, // 7: proto.AttachmentService.UpdateVisibility:output_type -> proto.UpdateVisibilityResponse
7, // 8: proto.AttachmentService.UpdateUsage:output_type -> proto.UpdateUsageResponse
9, // 9: proto.AttachmentService.DeleteAttachment:output_type -> proto.DeleteAttachmentResponse
5, // [5:10] is the sub-list for method output_type
0, // [0:5] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_attachment_proto_init() }
func file_attachment_proto_init() {
if File_attachment_proto != nil {
return
}
file_attachment_proto_msgTypes[0].OneofWrappers = []any{}
file_attachment_proto_msgTypes[1].OneofWrappers = []any{}
file_attachment_proto_msgTypes[2].OneofWrappers = []any{}
file_attachment_proto_msgTypes[4].OneofWrappers = []any{}
file_attachment_proto_msgTypes[8].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_attachment_proto_rawDesc), len(file_attachment_proto_rawDesc)),
NumEnums: 0,
NumMessages: 10,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_attachment_proto_goTypes,
DependencyIndexes: file_attachment_proto_depIdxs,
MessageInfos: file_attachment_proto_msgTypes,
}.Build()
File_attachment_proto = out.File
file_attachment_proto_goTypes = nil
file_attachment_proto_depIdxs = nil
}

View File

@@ -0,0 +1,64 @@
syntax = "proto3";
option go_package = ".;proto";
package proto;
service AttachmentService {
rpc GetAttachment(GetAttachmentRequest) returns (GetAttachmentResponse) {}
rpc ListAttachment(ListAttachmentRequest) returns (ListAttachmentResponse) {}
rpc UpdateVisibility(UpdateVisibilityRequest) returns (UpdateVisibilityResponse) {}
rpc UpdateUsage(UpdateUsageRequest) returns (UpdateUsageResponse) {}
rpc DeleteAttachment(DeleteAttachmentRequest) returns (DeleteAttachmentResponse) {}
}
message GetAttachmentRequest {
optional uint64 id = 1;
optional string rid = 2;
optional uint64 user_id = 3;
}
message GetAttachmentResponse {
optional bytes attachment = 1;
}
message ListAttachmentRequest {
repeated uint64 id = 1;
repeated string rid = 2;
optional uint64 user_id = 3;
}
message ListAttachmentResponse {
repeated bytes attachments = 1;
}
message UpdateVisibilityRequest {
repeated uint64 id = 1;
repeated string rid = 2;
bool is_indexable = 3;
optional uint64 user_id = 4;
}
message UpdateVisibilityResponse {
int32 count = 1;
}
message UpdateUsageRequest {
repeated uint64 id = 1;
repeated string rid = 2;
int64 delta = 3;
}
message UpdateUsageResponse {
int32 count = 1;
}
message DeleteAttachmentRequest {
repeated uint64 id = 1;
repeated string rid = 2;
optional uint64 user_id = 3;
}
message DeleteAttachmentResponse {
int32 count = 1;
}

View File

@@ -0,0 +1,273 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.29.3
// source: attachment.proto
package proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
AttachmentService_GetAttachment_FullMethodName = "/proto.AttachmentService/GetAttachment"
AttachmentService_ListAttachment_FullMethodName = "/proto.AttachmentService/ListAttachment"
AttachmentService_UpdateVisibility_FullMethodName = "/proto.AttachmentService/UpdateVisibility"
AttachmentService_UpdateUsage_FullMethodName = "/proto.AttachmentService/UpdateUsage"
AttachmentService_DeleteAttachment_FullMethodName = "/proto.AttachmentService/DeleteAttachment"
)
// AttachmentServiceClient is the client API for AttachmentService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AttachmentServiceClient interface {
GetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*GetAttachmentResponse, error)
ListAttachment(ctx context.Context, in *ListAttachmentRequest, opts ...grpc.CallOption) (*ListAttachmentResponse, error)
UpdateVisibility(ctx context.Context, in *UpdateVisibilityRequest, opts ...grpc.CallOption) (*UpdateVisibilityResponse, error)
UpdateUsage(ctx context.Context, in *UpdateUsageRequest, opts ...grpc.CallOption) (*UpdateUsageResponse, error)
DeleteAttachment(ctx context.Context, in *DeleteAttachmentRequest, opts ...grpc.CallOption) (*DeleteAttachmentResponse, error)
}
type attachmentServiceClient struct {
cc grpc.ClientConnInterface
}
func NewAttachmentServiceClient(cc grpc.ClientConnInterface) AttachmentServiceClient {
return &attachmentServiceClient{cc}
}
func (c *attachmentServiceClient) GetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*GetAttachmentResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetAttachmentResponse)
err := c.cc.Invoke(ctx, AttachmentService_GetAttachment_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *attachmentServiceClient) ListAttachment(ctx context.Context, in *ListAttachmentRequest, opts ...grpc.CallOption) (*ListAttachmentResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListAttachmentResponse)
err := c.cc.Invoke(ctx, AttachmentService_ListAttachment_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *attachmentServiceClient) UpdateVisibility(ctx context.Context, in *UpdateVisibilityRequest, opts ...grpc.CallOption) (*UpdateVisibilityResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateVisibilityResponse)
err := c.cc.Invoke(ctx, AttachmentService_UpdateVisibility_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *attachmentServiceClient) UpdateUsage(ctx context.Context, in *UpdateUsageRequest, opts ...grpc.CallOption) (*UpdateUsageResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateUsageResponse)
err := c.cc.Invoke(ctx, AttachmentService_UpdateUsage_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, in *DeleteAttachmentRequest, opts ...grpc.CallOption) (*DeleteAttachmentResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteAttachmentResponse)
err := c.cc.Invoke(ctx, AttachmentService_DeleteAttachment_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// AttachmentServiceServer is the server API for AttachmentService service.
// All implementations must embed UnimplementedAttachmentServiceServer
// for forward compatibility.
type AttachmentServiceServer interface {
GetAttachment(context.Context, *GetAttachmentRequest) (*GetAttachmentResponse, error)
ListAttachment(context.Context, *ListAttachmentRequest) (*ListAttachmentResponse, error)
UpdateVisibility(context.Context, *UpdateVisibilityRequest) (*UpdateVisibilityResponse, error)
UpdateUsage(context.Context, *UpdateUsageRequest) (*UpdateUsageResponse, error)
DeleteAttachment(context.Context, *DeleteAttachmentRequest) (*DeleteAttachmentResponse, error)
mustEmbedUnimplementedAttachmentServiceServer()
}
// UnimplementedAttachmentServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAttachmentServiceServer struct{}
func (UnimplementedAttachmentServiceServer) GetAttachment(context.Context, *GetAttachmentRequest) (*GetAttachmentResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAttachment not implemented")
}
func (UnimplementedAttachmentServiceServer) ListAttachment(context.Context, *ListAttachmentRequest) (*ListAttachmentResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListAttachment not implemented")
}
func (UnimplementedAttachmentServiceServer) UpdateVisibility(context.Context, *UpdateVisibilityRequest) (*UpdateVisibilityResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateVisibility not implemented")
}
func (UnimplementedAttachmentServiceServer) UpdateUsage(context.Context, *UpdateUsageRequest) (*UpdateUsageResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateUsage not implemented")
}
func (UnimplementedAttachmentServiceServer) DeleteAttachment(context.Context, *DeleteAttachmentRequest) (*DeleteAttachmentResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteAttachment not implemented")
}
func (UnimplementedAttachmentServiceServer) mustEmbedUnimplementedAttachmentServiceServer() {}
func (UnimplementedAttachmentServiceServer) testEmbeddedByValue() {}
// UnsafeAttachmentServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AttachmentServiceServer will
// result in compilation errors.
type UnsafeAttachmentServiceServer interface {
mustEmbedUnimplementedAttachmentServiceServer()
}
func RegisterAttachmentServiceServer(s grpc.ServiceRegistrar, srv AttachmentServiceServer) {
// If the following call pancis, it indicates UnimplementedAttachmentServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AttachmentService_ServiceDesc, srv)
}
func _AttachmentService_GetAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetAttachmentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AttachmentServiceServer).GetAttachment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AttachmentService_GetAttachment_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AttachmentServiceServer).GetAttachment(ctx, req.(*GetAttachmentRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AttachmentService_ListAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListAttachmentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AttachmentServiceServer).ListAttachment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AttachmentService_ListAttachment_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AttachmentServiceServer).ListAttachment(ctx, req.(*ListAttachmentRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AttachmentService_UpdateVisibility_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateVisibilityRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AttachmentServiceServer).UpdateVisibility(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AttachmentService_UpdateVisibility_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AttachmentServiceServer).UpdateVisibility(ctx, req.(*UpdateVisibilityRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AttachmentService_UpdateUsage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateUsageRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AttachmentServiceServer).UpdateUsage(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AttachmentService_UpdateUsage_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AttachmentServiceServer).UpdateUsage(ctx, req.(*UpdateUsageRequest))
}
return interceptor(ctx, in, info, handler)
}
func _AttachmentService_DeleteAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteAttachmentRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AttachmentServiceServer).DeleteAttachment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: AttachmentService_DeleteAttachment_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AttachmentServiceServer).DeleteAttachment(ctx, req.(*DeleteAttachmentRequest))
}
return interceptor(ctx, in, info, handler)
}
// AttachmentService_ServiceDesc is the grpc.ServiceDesc for AttachmentService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AttachmentService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "proto.AttachmentService",
HandlerType: (*AttachmentServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetAttachment",
Handler: _AttachmentService_GetAttachment_Handler,
},
{
MethodName: "ListAttachment",
Handler: _AttachmentService_ListAttachment_Handler,
},
{
MethodName: "UpdateVisibility",
Handler: _AttachmentService_UpdateVisibility_Handler,
},
{
MethodName: "UpdateUsage",
Handler: _AttachmentService_UpdateUsage_Handler,
},
{
MethodName: "DeleteAttachment",
Handler: _AttachmentService_DeleteAttachment_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "attachment.proto",
}

View File

@@ -1,349 +0,0 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.33.0
// protoc v5.26.1
// source: attachments.proto
package proto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Attachment struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id uint64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Uuid string `protobuf:"bytes,2,opt,name=uuid,proto3" json:"uuid,omitempty"`
Size int64 `protobuf:"varint,3,opt,name=size,proto3" json:"size,omitempty"`
Name string `protobuf:"bytes,4,opt,name=name,proto3" json:"name,omitempty"`
Alt string `protobuf:"bytes,5,opt,name=alt,proto3" json:"alt,omitempty"`
Usage string `protobuf:"bytes,6,opt,name=usage,proto3" json:"usage,omitempty"`
Mimetype string `protobuf:"bytes,7,opt,name=mimetype,proto3" json:"mimetype,omitempty"`
Hash string `protobuf:"bytes,8,opt,name=hash,proto3" json:"hash,omitempty"`
Destination string `protobuf:"bytes,9,opt,name=destination,proto3" json:"destination,omitempty"`
Metadata []byte `protobuf:"bytes,10,opt,name=metadata,proto3" json:"metadata,omitempty"`
IsMature bool `protobuf:"varint,11,opt,name=is_mature,json=isMature,proto3" json:"is_mature,omitempty"`
AccountId uint64 `protobuf:"varint,12,opt,name=account_id,json=accountId,proto3" json:"account_id,omitempty"`
}
func (x *Attachment) Reset() {
*x = Attachment{}
if protoimpl.UnsafeEnabled {
mi := &file_attachments_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Attachment) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Attachment) ProtoMessage() {}
func (x *Attachment) ProtoReflect() protoreflect.Message {
mi := &file_attachments_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Attachment.ProtoReflect.Descriptor instead.
func (*Attachment) Descriptor() ([]byte, []int) {
return file_attachments_proto_rawDescGZIP(), []int{0}
}
func (x *Attachment) GetId() uint64 {
if x != nil {
return x.Id
}
return 0
}
func (x *Attachment) GetUuid() string {
if x != nil {
return x.Uuid
}
return ""
}
func (x *Attachment) GetSize() int64 {
if x != nil {
return x.Size
}
return 0
}
func (x *Attachment) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Attachment) GetAlt() string {
if x != nil {
return x.Alt
}
return ""
}
func (x *Attachment) GetUsage() string {
if x != nil {
return x.Usage
}
return ""
}
func (x *Attachment) GetMimetype() string {
if x != nil {
return x.Mimetype
}
return ""
}
func (x *Attachment) GetHash() string {
if x != nil {
return x.Hash
}
return ""
}
func (x *Attachment) GetDestination() string {
if x != nil {
return x.Destination
}
return ""
}
func (x *Attachment) GetMetadata() []byte {
if x != nil {
return x.Metadata
}
return nil
}
func (x *Attachment) GetIsMature() bool {
if x != nil {
return x.IsMature
}
return false
}
func (x *Attachment) GetAccountId() uint64 {
if x != nil {
return x.AccountId
}
return 0
}
type AttachmentLookupRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id *uint64 `protobuf:"varint,1,opt,name=id,proto3,oneof" json:"id,omitempty"`
Uuid *string `protobuf:"bytes,2,opt,name=uuid,proto3,oneof" json:"uuid,omitempty"`
Usage *string `protobuf:"bytes,3,opt,name=usage,proto3,oneof" json:"usage,omitempty"`
}
func (x *AttachmentLookupRequest) Reset() {
*x = AttachmentLookupRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_attachments_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AttachmentLookupRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AttachmentLookupRequest) ProtoMessage() {}
func (x *AttachmentLookupRequest) ProtoReflect() protoreflect.Message {
mi := &file_attachments_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AttachmentLookupRequest.ProtoReflect.Descriptor instead.
func (*AttachmentLookupRequest) Descriptor() ([]byte, []int) {
return file_attachments_proto_rawDescGZIP(), []int{1}
}
func (x *AttachmentLookupRequest) GetId() uint64 {
if x != nil && x.Id != nil {
return *x.Id
}
return 0
}
func (x *AttachmentLookupRequest) GetUuid() string {
if x != nil && x.Uuid != nil {
return *x.Uuid
}
return ""
}
func (x *AttachmentLookupRequest) GetUsage() string {
if x != nil && x.Usage != nil {
return *x.Usage
}
return ""
}
var File_attachments_proto protoreflect.FileDescriptor
var file_attachments_proto_rawDesc = []byte{
0x0a, 0x11, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x12, 0x05, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74,
0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xaa, 0x02, 0x0a, 0x0a, 0x41, 0x74, 0x74, 0x61,
0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x75, 0x69, 0x64, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x75, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69,
0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x12,
0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61,
0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52,
0x03, 0x61, 0x6c, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6d, 0x69,
0x6d, 0x65, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6d, 0x69,
0x6d, 0x65, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x08,
0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65,
0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52,
0x0b, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08,
0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08,
0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x6d,
0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x4d,
0x61, 0x74, 0x75, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x5f, 0x69, 0x64, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x49, 0x64, 0x22, 0x7c, 0x0a, 0x17, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65,
0x6e, 0x74, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x48, 0x00, 0x52, 0x02, 0x69,
0x64, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x75, 0x75, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x48, 0x01, 0x52, 0x04, 0x75, 0x75, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a,
0x05, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05,
0x75, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x05, 0x0a, 0x03, 0x5f, 0x69, 0x64, 0x42,
0x07, 0x0a, 0x05, 0x5f, 0x75, 0x75, 0x69, 0x64, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x75, 0x73, 0x61,
0x67, 0x65, 0x32, 0xa6, 0x01, 0x0a, 0x0b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e,
0x74, 0x73, 0x12, 0x44, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d,
0x65, 0x6e, 0x74, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x74, 0x74, 0x61,
0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x11, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x74, 0x74, 0x61,
0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x00, 0x12, 0x51, 0x0a, 0x15, 0x43, 0x68, 0x65, 0x63,
0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x78, 0x69, 0x73, 0x74,
0x73, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68,
0x6d, 0x65, 0x6e, 0x74, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2e,
0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_attachments_proto_rawDescOnce sync.Once
file_attachments_proto_rawDescData = file_attachments_proto_rawDesc
)
func file_attachments_proto_rawDescGZIP() []byte {
file_attachments_proto_rawDescOnce.Do(func() {
file_attachments_proto_rawDescData = protoimpl.X.CompressGZIP(file_attachments_proto_rawDescData)
})
return file_attachments_proto_rawDescData
}
var file_attachments_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_attachments_proto_goTypes = []interface{}{
(*Attachment)(nil), // 0: proto.Attachment
(*AttachmentLookupRequest)(nil), // 1: proto.AttachmentLookupRequest
(*emptypb.Empty)(nil), // 2: google.protobuf.Empty
}
var file_attachments_proto_depIdxs = []int32{
1, // 0: proto.Attachments.GetAttachment:input_type -> proto.AttachmentLookupRequest
1, // 1: proto.Attachments.CheckAttachmentExists:input_type -> proto.AttachmentLookupRequest
0, // 2: proto.Attachments.GetAttachment:output_type -> proto.Attachment
2, // 3: proto.Attachments.CheckAttachmentExists:output_type -> google.protobuf.Empty
2, // [2:4] is the sub-list for method output_type
0, // [0:2] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_attachments_proto_init() }
func file_attachments_proto_init() {
if File_attachments_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_attachments_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Attachment); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_attachments_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AttachmentLookupRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_attachments_proto_msgTypes[1].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_attachments_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_attachments_proto_goTypes,
DependencyIndexes: file_attachments_proto_depIdxs,
MessageInfos: file_attachments_proto_msgTypes,
}.Build()
File_attachments_proto = out.File
file_attachments_proto_rawDesc = nil
file_attachments_proto_goTypes = nil
file_attachments_proto_depIdxs = nil
}

View File

@@ -1,33 +0,0 @@
syntax = "proto3";
option go_package = ".;proto";
import "google/protobuf/empty.proto";
package proto;
service Attachments {
rpc GetAttachment(AttachmentLookupRequest) returns (Attachment) {}
rpc CheckAttachmentExists(AttachmentLookupRequest) returns (google.protobuf.Empty) {}
}
message Attachment {
uint64 id = 1;
string uuid = 2;
int64 size = 3;
string name = 4;
string alt = 5;
string usage = 6;
string mimetype = 7;
string hash = 8;
string destination = 9;
bytes metadata = 10;
bool is_mature = 11;
uint64 account_id = 12;
}
message AttachmentLookupRequest {
optional uint64 id = 1;
optional string uuid = 2;
optional string usage = 3;
}

View File

@@ -1,147 +0,0 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
// - protoc v5.26.1
// source: attachments.proto
package proto
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
emptypb "google.golang.org/protobuf/types/known/emptypb"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
const (
Attachments_GetAttachment_FullMethodName = "/proto.Attachments/GetAttachment"
Attachments_CheckAttachmentExists_FullMethodName = "/proto.Attachments/CheckAttachmentExists"
)
// AttachmentsClient is the client API for Attachments service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type AttachmentsClient interface {
GetAttachment(ctx context.Context, in *AttachmentLookupRequest, opts ...grpc.CallOption) (*Attachment, error)
CheckAttachmentExists(ctx context.Context, in *AttachmentLookupRequest, opts ...grpc.CallOption) (*emptypb.Empty, error)
}
type attachmentsClient struct {
cc grpc.ClientConnInterface
}
func NewAttachmentsClient(cc grpc.ClientConnInterface) AttachmentsClient {
return &attachmentsClient{cc}
}
func (c *attachmentsClient) GetAttachment(ctx context.Context, in *AttachmentLookupRequest, opts ...grpc.CallOption) (*Attachment, error) {
out := new(Attachment)
err := c.cc.Invoke(ctx, Attachments_GetAttachment_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *attachmentsClient) CheckAttachmentExists(ctx context.Context, in *AttachmentLookupRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) {
out := new(emptypb.Empty)
err := c.cc.Invoke(ctx, Attachments_CheckAttachmentExists_FullMethodName, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// AttachmentsServer is the server API for Attachments service.
// All implementations must embed UnimplementedAttachmentsServer
// for forward compatibility
type AttachmentsServer interface {
GetAttachment(context.Context, *AttachmentLookupRequest) (*Attachment, error)
CheckAttachmentExists(context.Context, *AttachmentLookupRequest) (*emptypb.Empty, error)
mustEmbedUnimplementedAttachmentsServer()
}
// UnimplementedAttachmentsServer must be embedded to have forward compatible implementations.
type UnimplementedAttachmentsServer struct {
}
func (UnimplementedAttachmentsServer) GetAttachment(context.Context, *AttachmentLookupRequest) (*Attachment, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetAttachment not implemented")
}
func (UnimplementedAttachmentsServer) CheckAttachmentExists(context.Context, *AttachmentLookupRequest) (*emptypb.Empty, error) {
return nil, status.Errorf(codes.Unimplemented, "method CheckAttachmentExists not implemented")
}
func (UnimplementedAttachmentsServer) mustEmbedUnimplementedAttachmentsServer() {}
// UnsafeAttachmentsServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AttachmentsServer will
// result in compilation errors.
type UnsafeAttachmentsServer interface {
mustEmbedUnimplementedAttachmentsServer()
}
func RegisterAttachmentsServer(s grpc.ServiceRegistrar, srv AttachmentsServer) {
s.RegisterService(&Attachments_ServiceDesc, srv)
}
func _Attachments_GetAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AttachmentLookupRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AttachmentsServer).GetAttachment(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Attachments_GetAttachment_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AttachmentsServer).GetAttachment(ctx, req.(*AttachmentLookupRequest))
}
return interceptor(ctx, in, info, handler)
}
func _Attachments_CheckAttachmentExists_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(AttachmentLookupRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(AttachmentsServer).CheckAttachmentExists(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: Attachments_CheckAttachmentExists_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(AttachmentsServer).CheckAttachmentExists(ctx, req.(*AttachmentLookupRequest))
}
return interceptor(ctx, in, info, handler)
}
// Attachments_ServiceDesc is the grpc.ServiceDesc for Attachments service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var Attachments_ServiceDesc = grpc.ServiceDesc{
ServiceName: "proto.Attachments",
HandlerType: (*AttachmentsServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetAttachment",
Handler: _Attachments_GetAttachment_Handler,
},
{
MethodName: "CheckAttachmentExists",
Handler: _Attachments_CheckAttachmentExists_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "attachments.proto",
}

View File

@@ -1,38 +1,38 @@
id = "paperclip01"
bind = "0.0.0.0:8443"
grpc_bind = "0.0.0.0:7443"
domain = "usercontent.solsynth.dev"
secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi"
bind = "0.0.0.0:8004"
grpc_bind = "0.0.0.0:7004"
preferred_destination = "local"
accepts_usage = ["p.avatar", "p.banner", "i.attachment", "m.attachment"]
nexus_addr = "localhost:7001"
preferred_destination = 1
[workers]
files_deletion = 4
files_analyze = 4
[debug]
database = false
print_routes = false
[dealer]
addr = "127.0.0.1:7442"
[performance]
file_chunk_size = 26214400
[security]
cookie_domain = "localhost"
cookie_samesite = "Lax"
access_token_duration = 300
refresh_token_duration = 2592000
[database]
dsn = "host=localhost user=postgres password=password dbname=hy_paperclip port=5432 sslmode=disable"
prefix = "paperclip_"
[destinations.local]
[[destinations]]
type = "local"
path = "uploads"
[[destinations]]
type = "local"
path = "uploads/permanent"
access_baseurl = "http://192.168.50.133:8004"
image_proxy_baseurl = "https://io.sn.solsynth.dev"
[destinations.s3]
type = "s3"
bucket = "bucket"
endpoint = "s3.ap-east-1.amazonaws.com"
secret_id = "secret"
secret_key = "secret"
enable_ssl = true
[traffic]
maximum_size = 20971520
minimum_size = 1048576
[security]
internal_public_key = "keys/internal_public_key.pem"
[payment]
discount = 52428800