mirror of
https://github.com/kyantech/Palmr.git
synced 2025-10-23 06:11:58 +00:00
Compare commits
54 Commits
v3.1.8-bet
...
v3.2.3-bet
Author | SHA1 | Date | |
---|---|---|---|
|
94e021d8c6 | ||
|
95ac0f195b | ||
|
d6c9b0d7d2 | ||
|
59f9e19ffb | ||
|
6086d2a0ac | ||
|
f3aeaf66df | ||
|
6b979a22fb | ||
|
e4bae380c9 | ||
|
3117904009 | ||
|
331624e2f2 | ||
|
b078e94189 | ||
|
ba512ebe95 | ||
|
bd4212b44c | ||
|
b699bffb5b | ||
|
a755c5324f | ||
|
9072e7e866 | ||
|
d3d1057ba8 | ||
|
24eda85fdc | ||
|
d49d15ac9b | ||
|
e7b2062764 | ||
|
f21f972825 | ||
|
4f4e4a079e | ||
|
6fbb9aa9da | ||
|
0e610d002c | ||
|
abd8366e94 | ||
|
5d8c243125 | ||
|
d23af700da | ||
|
494161eb47 | ||
|
6a9728be4b | ||
|
5e889956c7 | ||
|
5afc6ea271 | ||
|
cc368377c2 | ||
|
51764be7d4 | ||
|
fe598b4a30 | ||
|
80286e57d9 | ||
|
9f36a48d15 | ||
|
94286e8452 | ||
|
0ce2d6a998 | ||
|
9e15fd7d2e | ||
|
736348ebe8 | ||
|
ddb981cba2 | ||
|
724452fb40 | ||
|
a2ac6a6268 | ||
|
aecda25b25 | ||
|
0f22b0bb23 | ||
|
edf6d70d69 | ||
|
a2ecd2e221 | ||
|
2f022cae5d | ||
|
bb3669f5b2 | ||
|
87fd8caf2c | ||
|
e8087a7c01 | ||
|
4075a7df29 | ||
|
c081b6f764 | ||
|
ecaa6d0321 |
212
LICENSE
212
LICENSE
@@ -1,40 +1,190 @@
|
||||
Kyantech-Permissive License (Based on BSD 2-Clause)
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2025, Daniel Luiz Alves (danielalves96) - Kyantech Solutions
|
||||
All rights reserved.
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted for any purpose — private, commercial,
|
||||
educational, governmental — **fully free and unrestricted**, provided
|
||||
that the following conditions are met:
|
||||
1. Definitions.
|
||||
|
||||
1. Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions, and the following disclaimer.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions, and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
3. **If this software (or derivative works) is used in any public-facing
|
||||
interface** — such as websites, apps, dashboards, admin panels, or
|
||||
similar — a **simple credit** must appear in the footer or similar
|
||||
location. The credit text should read:
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
> “Powered by Kyantech Solutions · https://kyantech.com.br”
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
This credit must be reasonably visible but **must not interfere** with
|
||||
your UI, branding, or user experience. You may style it to match your
|
||||
own design and choose its size, placement, or color.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
---
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
Copyright 2025 Daniel Luiz Alves (danielalves96) - Kyantech Solutions, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
296
README.md
296
README.md
@@ -1,142 +1,154 @@
|
||||
# 🌴 Palmr. - Open-Source File Transfer
|
||||
|
||||
<p align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749825361/Group_47_1_bcx8gw.png" alt="Palmr Banner" style="width: 100%;"/>
|
||||
</p>
|
||||
|
||||
**Palmr.** is a **flexible** and **open-source** alternative to file transfer services like **WeTransfer**, **SendGB**, **Send Anywhere**, and **Files.fm**.
|
||||
|
||||
|
||||
🔗 **For detailed documentation visit:** [Palmr. - Documentation](https://palmr.kyantech.com.br)
|
||||
|
||||
## 📌 Why Choose Palmr.?
|
||||
|
||||
- **Self-hosted** – Deploy on your own server or VPS.
|
||||
- **Full control** – No third-party dependencies, ensuring privacy and security.
|
||||
- **No artificial limits** – Share files without hidden restrictions or fees.
|
||||
- **Simple deployment** – SQLite database and filesystem storage for easy setup.
|
||||
- **Scalable storage** – Optional S3-compatible object storage for enterprise needs.
|
||||
|
||||
## 🚀 Technologies Used
|
||||
|
||||
### **Palmr.** is built with a focus on **performance**, **scalability**, and **security**.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1745548231/Palmr./Captura_de_Tela_2025-04-24_a%CC%80s_23.24.26_kr4hsl.png" style="width: 100%; border-radius: 15px;" />
|
||||
</div>
|
||||
|
||||
|
||||
### **Backend & API**
|
||||
- **Fastify (Node.js)** – High-performance API framework with built-in schema validation.
|
||||
- **SQLite** – Lightweight, reliable database with zero-configuration setup.
|
||||
- **Filesystem Storage** – Direct file storage with optional S3-compatible object storage.
|
||||
|
||||
### **Frontend**
|
||||
- **NextJS 15 + TypeScript + Shadcn/ui** – Modern and fast web interface.
|
||||
|
||||
|
||||
## 🛠️ How It Works
|
||||
|
||||
1. **Web Interface** → Built with Next, React and TypeScript for a seamless user experience.
|
||||
2. **Backend API** → Fastify handles requests and manages file operations.
|
||||
3. **Database** → SQLite stores metadata and transactional data with zero configuration.
|
||||
4. **Storage** → Filesystem storage ensures reliable file storage with optional S3-compatible object storage for scalability.
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824929/Login_veq6e7.png" alt="Login Page" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Login Page</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824929/Home_lzvfzu.png" alt="Home Page" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Home Page</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Dashboard_uycmxb.png" alt="Dashboard" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Dashboard</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824929/Profile_wvnlzw.png" alt="Profile Page" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Profile Page</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Files_List_ztwr1e.png" alt="Files List View" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Files List View</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Files_Cards_pwsh5e.png" alt="Files Card View" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Files Card View</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824927/Shares_cgplgw.png" alt="Shares Management" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Shares Management</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Reive_Files_uhkeyc.png" alt="Receive Files" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Receive Files</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824927/Default_Reverse_xedmhw.png" alt="Reverse Share" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Reverse Share</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Settings_oampxr.png" alt="Settings Panel" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Settings Panel</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/User_Management_xjbfhn.png" alt="User Management" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>User Management</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Forgot_Password_jcz9ad.png" alt="Forgot Password" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Forgot Password</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/WeTransfer_Reverse_u0g7eb.png" alt="Forgot Password" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Reverse Share (WeTransfer Style)</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## 👨💻 Core Maintainers
|
||||
|
||||
| [**Daniel Luiz Alves**](https://github.com/danielalves96) |
|
||||
|------------------|
|
||||
| <img src="https://github.com/danielalves96.png" width="150px" alt="Daniel Luiz Alves" /> |
|
||||
|
||||
</br>
|
||||
|
||||
## 🤝 Supporters
|
||||
|
||||
[<img src="https://i.ibb.co/nMN40STL/Repoflow.png" width="200px" alt="Daniel Luiz Alves" />](https://www.repoflow.io/)
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
<a href="https://www.star-history.com/#kyantech/Palmr&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kyantech/Palmr&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kyantech/Palmr&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kyantech/Palmr&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 🛠️ Contributing
|
||||
|
||||
For contribution guidelines, please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
||||
|
||||
# 🌴 Palmr. - Open-Source File Transfer
|
||||
|
||||
<p align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749825361/Group_47_1_bcx8gw.png" alt="Palmr Banner" style="width: 100%;"/>
|
||||
</p>
|
||||
|
||||
**Palmr.** is a **flexible** and **open-source** alternative to file transfer services like **WeTransfer**, **SendGB**, **Send Anywhere**, and **Files.fm**.
|
||||
|
||||
<div align="center">
|
||||
<div style="background: linear-gradient(135deg, #ff4757, #ff3838); padding: 20px; border-radius: 12px; margin: 20px 0; box-shadow: 0 4px 15px rgba(255, 71, 87, 0.3); border: 2px solid #ff3838;">
|
||||
<h3 style="color: white; margin: 0 0 10px 0; font-size: 18px; font-weight: bold;">
|
||||
⚠️ BETA VERSION
|
||||
</h3>
|
||||
<p style="color: white; margin: 0; font-size: 14px; opacity: 0.95;">
|
||||
<strong>This project is currently in beta phase.</strong><br>
|
||||
Not recommended for production environments.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
🔗 **For detailed documentation visit:** [Palmr. - Documentation](https://palmr.kyantech.com.br)
|
||||
|
||||
## 📌 Why Choose Palmr.?
|
||||
|
||||
- **Self-hosted** – Deploy on your own server or VPS.
|
||||
- **Full control** – No third-party dependencies, ensuring privacy and security.
|
||||
- **No artificial limits** – Share files without hidden restrictions or fees.
|
||||
- **Folder organization** – Create folders to organize and share files.
|
||||
- **Simple deployment** – SQLite database and filesystem storage for easy setup.
|
||||
- **Scalable storage** – Optional S3-compatible object storage for enterprise needs.
|
||||
|
||||
## 🚀 Technologies Used
|
||||
|
||||
### **Palmr.** is built with a focus on **performance**, **scalability**, and **security**.
|
||||
|
||||
<div align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1745548231/Palmr./Captura_de_Tela_2025-04-24_a%CC%80s_23.24.26_kr4hsl.png" style="width: 100%; border-radius: 15px;" />
|
||||
</div>
|
||||
|
||||
|
||||
### **Backend & API**
|
||||
- **Fastify (Node.js)** – High-performance API framework with built-in schema validation.
|
||||
- **SQLite** – Lightweight, reliable database with zero-configuration setup.
|
||||
- **Filesystem Storage** – Direct file storage with optional S3-compatible object storage.
|
||||
|
||||
### **Frontend**
|
||||
- **NextJS 15 + TypeScript + Shadcn/ui** – Modern and fast web interface.
|
||||
|
||||
|
||||
## 🛠️ How It Works
|
||||
|
||||
1. **Web Interface** → Built with Next, React and TypeScript for a seamless user experience.
|
||||
2. **Backend API** → Fastify handles requests and manages file operations.
|
||||
3. **Database** → SQLite stores metadata and transactional data with zero configuration.
|
||||
4. **Storage** → Filesystem storage ensures reliable file storage with optional S3-compatible object storage for scalability.
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824929/Login_veq6e7.png" alt="Login Page" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Login Page</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824929/Home_lzvfzu.png" alt="Home Page" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Home Page</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Dashboard_uycmxb.png" alt="Dashboard" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Dashboard</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824929/Profile_wvnlzw.png" alt="Profile Page" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Profile Page</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Files_List_ztwr1e.png" alt="Files List View" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Files List View</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Files_Cards_pwsh5e.png" alt="Files Card View" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Files Card View</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824927/Shares_cgplgw.png" alt="Shares Management" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Shares Management</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Reive_Files_uhkeyc.png" alt="Receive Files" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Receive Files</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824927/Default_Reverse_xedmhw.png" alt="Reverse Share" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Reverse Share</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Settings_oampxr.png" alt="Settings Panel" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Settings Panel</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/User_Management_xjbfhn.png" alt="User Management" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>User Management</strong>
|
||||
</td>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/Forgot_Password_jcz9ad.png" alt="Forgot Password" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Forgot Password</strong>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<img src="https://res.cloudinary.com/technical-intelligence/image/upload/v1749824928/WeTransfer_Reverse_u0g7eb.png" alt="Forgot Password" style="width: 100%; border-radius: 8px;" />
|
||||
<br /><strong>Reverse Share (WeTransfer Style)</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
## 👨💻 Core Maintainers
|
||||
|
||||
| [**Daniel Luiz Alves**](https://github.com/danielalves96) |
|
||||
|------------------|
|
||||
| <img src="https://github.com/danielalves96.png" width="150px" alt="Daniel Luiz Alves" /> |
|
||||
|
||||
</br>
|
||||
|
||||
## 🤝 Supporters
|
||||
|
||||
[<img src="https://i.ibb.co/nMN40STL/Repoflow.png" width="200px" alt="Daniel Luiz Alves" />](https://www.repoflow.io/)
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
<a href="https://www.star-history.com/#kyantech/Palmr&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=kyantech/Palmr&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=kyantech/Palmr&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=kyantech/Palmr&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 🛠️ Contributing
|
||||
|
||||
For contribution guidelines, please refer to the [CONTRIBUTING.md](CONTRIBUTING.md) file.
|
||||
|
||||
|
@@ -105,6 +105,12 @@ The Palmr. API provides comprehensive access to all platform features:
|
||||
- **File management** - Rename, delete, and organize files
|
||||
- **Metadata access** - Retrieve file information and properties
|
||||
|
||||
### Folder operations
|
||||
|
||||
- **Create folders** - Build folder structures for organization
|
||||
- **Folder management** - Rename, move, delete folders
|
||||
- **Folder sharing** - Share folders with same controls as files
|
||||
|
||||
### Share management
|
||||
|
||||
- **Create shares** - Generate public links for file sharing
|
@@ -51,7 +51,7 @@ If you need to protect sensitive files at rest, you can enable encryption by set
|
||||
|
||||
For optimal performance with encryption enabled, ensure your hardware supports AES-NI acceleration (check with `cat /proc/cpuinfo | grep aes` on Linux).
|
||||
|
||||
As an alternative, consider using S3-compatible object storage (e.g., AWS S3 or MinIO), which can offload file storage from the local filesystem and potentially reduce local CPU overhead for encryption/decryption. See [S3 Providers](/docs/3.1-beta/s3-providers) for setup instructions.
|
||||
As an alternative, consider using S3-compatible object storage (e.g., AWS S3 or MinIO), which can offload file storage from the local filesystem and potentially reduce local CPU overhead for encryption/decryption. See [S3 Providers](/docs/3.2-beta/s3-providers) for setup instructions.
|
||||
|
||||
### Fastify + Zod + TypeScript
|
||||
|
||||
@@ -127,7 +127,7 @@ Palmr. is designed to be flexible in how you handle file storage:
|
||||
|
||||
**Optional S3-compatible storage:**
|
||||
|
||||
- Enable S3 storage by setting `ENABLE_S3=true`, look at [S3 Providers](/docs/3.1-beta/s3-providers) for more information.
|
||||
- Enable S3 storage by setting `ENABLE_S3=true`, look at [S3 Providers](/docs/3.2-beta/s3-providers) for more information.
|
||||
- Compatible with AWS S3, MinIO, and other S3-compatible services
|
||||
- Ideal for cloud deployments and distributed setups
|
||||
- Provides additional scalability and redundancy options
|
391
apps/docs/content/docs/3.2-beta/download-memory-management.mdx
Normal file
391
apps/docs/content/docs/3.2-beta/download-memory-management.mdx
Normal file
@@ -0,0 +1,391 @@
|
||||
---
|
||||
title: Memory Management
|
||||
icon: Download
|
||||
---
|
||||
|
||||
import { Callout } from "fumadocs-ui/components/callout";
|
||||
import { Tab, Tabs } from "fumadocs-ui/components/tabs";
|
||||
|
||||
Palmr implements an intelligent memory management system that prevents crashes during large file downloads (3GB+ by default), maintaining unlimited download capacity through adaptive resource control and an automatic queue system.
|
||||
|
||||
## How It Works
|
||||
|
||||
### Automatic Resource Detection
|
||||
|
||||
The system automatically detects available container/system memory and configures appropriate limits based on available infrastructure:
|
||||
|
||||
```typescript
|
||||
const totalMemoryGB = require("os").totalmem() / 1024 ** 3;
|
||||
```
|
||||
|
||||
### System Configuration
|
||||
|
||||
The system supports two configuration approaches that you can choose based on your needs:
|
||||
|
||||
<Tabs items={["Manual Configuration", "Auto-scaling (Default)"]}>
|
||||
<Tab value="Manual Configuration">
|
||||
Manually configure all parameters for total control over the system:
|
||||
|
||||
```bash
|
||||
# Custom configuration (overrides auto-scaling)
|
||||
DOWNLOAD_MAX_CONCURRENT=8 # Maximum simultaneous downloads
|
||||
DOWNLOAD_MEMORY_THRESHOLD_MB=1536 # Memory threshold in MB
|
||||
DOWNLOAD_QUEUE_SIZE=40 # Maximum queue size
|
||||
DOWNLOAD_AUTO_SCALE=false # Disable auto-scaling
|
||||
```
|
||||
|
||||
<Callout>
|
||||
Manual configuration offers total control and predictability for specific environments where you know exactly the available resources.
|
||||
</Callout>
|
||||
|
||||
</Tab>
|
||||
<Tab value="Auto-scaling (Default)">
|
||||
Automatic configuration based on detected system memory:
|
||||
|
||||
| Available Memory | Concurrent Downloads | Memory Threshold | Queue Size | Recommended Use |
|
||||
|------------------|----------------------|-------------------|------------|--------------------|
|
||||
| ≤ 2GB | 1 | 256MB | 5 | Development |
|
||||
| 2GB - 4GB | 2 | 512MB | 10 | Small Environment |
|
||||
| 4GB - 8GB | 3 | 1GB | 15 | Standard Production|
|
||||
| 8GB - 16GB | 5 | 2GB | 25 | High Performance |
|
||||
| > 16GB | 10 | 4GB | 50 | Enterprise |
|
||||
|
||||
<Callout>
|
||||
Auto-scaling automatically adapts to different environments without manual configuration, perfect for flexible deployment.
|
||||
</Callout>
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
<Callout type="info">If environment variables are configured, they take **priority** over auto-scaling.</Callout>
|
||||
|
||||
## Download Queue System
|
||||
|
||||
### How It Works
|
||||
|
||||
The memory management system only activates for files larger than the configured minimum size (3GB by default). Smaller files bypass the queue system entirely and download immediately without memory management.
|
||||
|
||||
When a user requests a download for a large file but all slots are occupied, the system automatically queues the download instead of returning a 429 error. The queue processes downloads in FIFO order (first in, first out).
|
||||
|
||||
### Practical Example
|
||||
|
||||
Consider a system with 8GB RAM (5 concurrent downloads, queue of 25, 3GB minimum) where users want to download files of various sizes:
|
||||
|
||||
```bash
|
||||
# Small files (< 3GB): Bypass queue entirely
|
||||
[DOWNLOAD MANAGER] File document.pdf (0.05GB) below threshold (3.0GB), bypassing queue
|
||||
|
||||
# Large files 1-5: Start immediately
|
||||
[DOWNLOAD MANAGER] Immediate start: 1734567890-abc123def
|
||||
[DOWNLOAD MANAGER] Starting video1.mp4 (5.2GB)
|
||||
|
||||
# Large files 6-10: Automatically queued
|
||||
[DOWNLOAD MANAGER] Queued: 1734567891-def456ghi (Position: 1/25)
|
||||
[DOWNLOAD MANAGER] Queued file: video2.mp4 (8.1GB)
|
||||
|
||||
# When download 1 finishes: download 6 starts automatically
|
||||
[DOWNLOAD MANAGER] Processing queue: 1734567891-def456ghi (4 remaining)
|
||||
[DOWNLOAD MANAGER] Starting queued file: video2.mp4 (8.1GB)
|
||||
```
|
||||
|
||||
### System Benefits
|
||||
|
||||
**User Experience**
|
||||
|
||||
- Users don't receive errors, they simply wait in queue
|
||||
- Downloads start automatically when slots become available
|
||||
- Transparent operation without client changes
|
||||
- Fair processing order with FIFO queue
|
||||
|
||||
**Technical Features**
|
||||
|
||||
- Limited buffers (64KB per stream) for controlled memory usage
|
||||
- Automatic backpressure control with pipeline streams
|
||||
- Adaptive memory throttling based on usage patterns
|
||||
- Forced garbage collection after large downloads
|
||||
- Smart timeout handling (30 minutes for queued downloads)
|
||||
- Automatic cleanup of orphaned downloads every 30 seconds
|
||||
|
||||
## Container Compatibility
|
||||
|
||||
The system works with Docker, Kubernetes, and any containerized environment:
|
||||
|
||||
<Tabs items={["Docker", "Kubernetes", "Docker Compose"]}>
|
||||
<Tab value="Docker">
|
||||
|
||||
```bash
|
||||
# Example: Container with 8GB
|
||||
docker run -m 8g palmr/server
|
||||
# Result: 5 concurrent downloads, queue of 25, threshold 2GB
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Kubernetes">
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: palmr-server
|
||||
resources:
|
||||
limits:
|
||||
memory: "4Gi" # Detects 4GB
|
||||
cpu: "2"
|
||||
requests:
|
||||
memory: "2Gi"
|
||||
cpu: "1"
|
||||
# Result: 3 concurrent downloads, queue of 15, threshold 1GB
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Docker Compose">
|
||||
|
||||
```yaml
|
||||
services:
|
||||
palmr-server:
|
||||
image: palmr/server
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 16G # Detects 16GB
|
||||
# Result: 10 concurrent downloads, queue of 50, threshold 4GB
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Configure the download memory management system using these environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------ | ---------- | ----------------------------------------------------- |
|
||||
| `DOWNLOAD_MAX_CONCURRENT` | auto-scale | Maximum number of simultaneous downloads |
|
||||
| `DOWNLOAD_MEMORY_THRESHOLD_MB` | auto-scale | Memory limit in MB before throttling |
|
||||
| `DOWNLOAD_QUEUE_SIZE` | auto-scale | Maximum download queue size |
|
||||
| `DOWNLOAD_AUTO_SCALE` | `true` | Enable/disable auto-scaling based on system memory |
|
||||
| `DOWNLOAD_MIN_FILE_SIZE_GB` | `3.0` | Minimum file size in GB to activate memory management |
|
||||
|
||||
### Configuration Examples by Scenario
|
||||
|
||||
<Tabs items={["Home Server", "Enterprise", "High Performance", "Conservative"]}>
|
||||
<Tab value="Home Server">
|
||||
Configuration optimized for personal use or small groups (4GB RAM):
|
||||
|
||||
```bash
|
||||
DOWNLOAD_MAX_CONCURRENT=2
|
||||
DOWNLOAD_MEMORY_THRESHOLD_MB=1024
|
||||
DOWNLOAD_QUEUE_SIZE=8
|
||||
DOWNLOAD_MIN_FILE_SIZE_GB=2.0
|
||||
DOWNLOAD_AUTO_SCALE=false
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Enterprise">
|
||||
Configuration for corporate environments with multiple users (16GB RAM):
|
||||
|
||||
```bash
|
||||
DOWNLOAD_MAX_CONCURRENT=12
|
||||
DOWNLOAD_MEMORY_THRESHOLD_MB=4096
|
||||
DOWNLOAD_QUEUE_SIZE=60
|
||||
DOWNLOAD_MIN_FILE_SIZE_GB=5.0
|
||||
DOWNLOAD_AUTO_SCALE=false
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="High Performance">
|
||||
Configuration for maximum performance and throughput (32GB RAM):
|
||||
|
||||
```bash
|
||||
DOWNLOAD_MAX_CONCURRENT=20
|
||||
DOWNLOAD_MEMORY_THRESHOLD_MB=8192
|
||||
DOWNLOAD_QUEUE_SIZE=100
|
||||
DOWNLOAD_MIN_FILE_SIZE_GB=10.0
|
||||
DOWNLOAD_AUTO_SCALE=false
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Conservative">
|
||||
For environments with limited or shared resources:
|
||||
|
||||
```bash
|
||||
DOWNLOAD_MAX_CONCURRENT=3
|
||||
DOWNLOAD_MEMORY_THRESHOLD_MB=1024
|
||||
DOWNLOAD_QUEUE_SIZE=15
|
||||
DOWNLOAD_MIN_FILE_SIZE_GB=1.0
|
||||
DOWNLOAD_AUTO_SCALE=false
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Additional Configuration
|
||||
|
||||
For optimal performance with large downloads, consider these additional settings:
|
||||
|
||||
```bash
|
||||
# Force garbage collection (recommended for large downloads)
|
||||
NODE_OPTIONS="--expose-gc"
|
||||
|
||||
# Adjust timeout for very large downloads
|
||||
KEEP_ALIVE_TIMEOUT=300000
|
||||
REQUEST_TIMEOUT=0
|
||||
```
|
||||
|
||||
## Monitoring and Logs
|
||||
|
||||
### System Logs
|
||||
|
||||
The system provides detailed logs to track operation:
|
||||
|
||||
```bash
|
||||
[DOWNLOAD MANAGER] System Memory: 8.0GB, Max Concurrent: 5, Memory Threshold: 2048MB, Queue Size: 25
|
||||
[DOWNLOAD] Requesting slot for 1734567890-abc123def: video.mp4 (15.2GB)
|
||||
[DOWNLOAD MANAGER] Queued: 1734567890-abc123def (Position: 3/25)
|
||||
[DOWNLOAD MANAGER] Processing queue: 1734567890-abc123def (2 remaining)
|
||||
[DOWNLOAD] Starting 1734567890-abc123def: video.mp4 (15.2GB)
|
||||
[MEMORY THROTTLE] video.mp4 - Pausing stream due to high memory usage: 1843MB
|
||||
[DOWNLOAD] Applying throttling: 100ms delay for 1734567890-abc123def
|
||||
```
|
||||
|
||||
### Configuration Validation
|
||||
|
||||
The system automatically validates configurations at startup and provides warnings or errors:
|
||||
|
||||
**Warnings**
|
||||
|
||||
- `DOWNLOAD_MAX_CONCURRENT > 50`: May cause performance issues
|
||||
- `DOWNLOAD_MEMORY_THRESHOLD_MB < 128MB`: Downloads may be throttled frequently
|
||||
- `DOWNLOAD_MEMORY_THRESHOLD_MB > 16GB`: System may run out of memory
|
||||
- `DOWNLOAD_QUEUE_SIZE > 1000`: May consume significant memory
|
||||
- `DOWNLOAD_QUEUE_SIZE < DOWNLOAD_MAX_CONCURRENT`: Queue smaller than concurrent downloads
|
||||
|
||||
**Errors**
|
||||
|
||||
- `DOWNLOAD_MAX_CONCURRENT < 1`: Invalid value
|
||||
- `DOWNLOAD_QUEUE_SIZE < 1`: Invalid value
|
||||
|
||||
## Queue Management APIs
|
||||
|
||||
The system provides REST APIs to monitor and manage the download queue:
|
||||
|
||||
### Get Queue Status
|
||||
|
||||
```http
|
||||
GET /api/filesystem/download-queue/status
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"queueLength": 3,
|
||||
"maxQueueSize": 25,
|
||||
"activeDownloads": 5,
|
||||
"maxConcurrent": 5,
|
||||
"queuedDownloads": [
|
||||
{
|
||||
"downloadId": "1734567890-abc123def",
|
||||
"position": 1,
|
||||
"waitTime": 45000,
|
||||
"fileName": "video.mp4",
|
||||
"fileSize": 16106127360
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "success"
|
||||
}
|
||||
```
|
||||
|
||||
### Cancel Download
|
||||
|
||||
```http
|
||||
DELETE /api/filesystem/download-queue/{downloadId}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"downloadId": "1734567890-abc123def",
|
||||
"message": "Download cancelled successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Clear Queue (Admin)
|
||||
|
||||
```http
|
||||
DELETE /api/filesystem/download-queue
|
||||
```
|
||||
|
||||
**Response:**
|
||||
|
||||
```json
|
||||
{
|
||||
"clearedCount": 8,
|
||||
"message": "Download queue cleared successfully"
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Downloads failing with "Download queue is full"**
|
||||
|
||||
_Cause:_ Too many simultaneous downloads with a full queue
|
||||
|
||||
_Solutions:_
|
||||
|
||||
- Wait for some downloads to finish
|
||||
- Check for orphaned downloads in queue
|
||||
- Consider increasing container resources
|
||||
- Use API to clear queue if necessary
|
||||
|
||||
**Downloads stay too long in queue**
|
||||
|
||||
_Cause:_ Active downloads are slow or stuck
|
||||
|
||||
_Solutions:_
|
||||
|
||||
- Check logs for orphaned downloads
|
||||
- Use API to cancel specific downloads
|
||||
- Check client network connections
|
||||
- Monitor memory throttling
|
||||
|
||||
**Very slow downloads**
|
||||
|
||||
_Cause:_ Active throttling due to high memory usage
|
||||
|
||||
_Solutions:_
|
||||
|
||||
- Check other processes consuming memory
|
||||
- Consider increasing container resources
|
||||
- Monitor throttling logs
|
||||
- Check number of simultaneous downloads
|
||||
|
||||
## Summary
|
||||
|
||||
This system enables unlimited downloads (including 50TB+ files) without compromising system stability through:
|
||||
|
||||
**Key Features**
|
||||
|
||||
- Auto-configuration based on available resources
|
||||
- Automatic FIFO queue system for pending downloads
|
||||
- Adaptive control of simultaneous downloads
|
||||
- Intelligent throttling when needed
|
||||
|
||||
**System Benefits**
|
||||
|
||||
- Management APIs to monitor and control queue
|
||||
- Automatic cleanup of resources and orphaned downloads
|
||||
- Full compatibility with Docker/Kubernetes
|
||||
- Perfect user experience with no 429 errors
|
||||
|
||||
The system maintains high performance for small/medium files while preventing crashes with gigantic files, offering a seamless experience where users never see 429 errors, they simply wait in queue until their download starts automatically.
|
@@ -49,6 +49,7 @@ The frontend is organized with:
|
||||
- **Custom hooks** to isolate logic and side effects
|
||||
- A **route protection system** using session cookies and middleware
|
||||
- A **file management interface** integrated with the backend
|
||||
- **Folder support** for organizing files hierarchically
|
||||
- A **reusable modal system** used for file actions, confirmations, and more
|
||||
- **Dynamic, locale-aware routing** using next-intl
|
||||
|
||||
@@ -68,7 +69,7 @@ Data is stored in **SQLite**, which handles user info, file metadata, session to
|
||||
Key features include:
|
||||
|
||||
- **Authentication/authorization** with JWT + cookie sessions
|
||||
- **File management logic** including uploads, deletes, and renames
|
||||
- **File management logic** including uploads, deletes, renames, and folders
|
||||
- **Storage operations** to handle file organization, usage tracking, and cleanup
|
||||
- A **share system** that generates tokenized public file links
|
||||
- Schema-based request validation for all endpoints
|
||||
@@ -106,9 +107,10 @@ Volumes are used to persist data locally, and containers are networked together
|
||||
|
||||
### File management
|
||||
|
||||
Files are at the heart of Palmr. Users can upload files via the frontend, and they're stored directly in the filesystem. The backend handles metadata (name, size, type, ownership), and also handles deletion, renaming, and public sharing. Every file operation is tracked, and all actions can be scoped per user.
|
||||
Files are at the heart of Palmr. Users can upload files via the frontend, and they're stored directly in the filesystem. Users can also create folders to organize files. The backend handles metadata (name, size, type, ownership), and also handles deletion, renaming, and public sharing. Every file operation is tracked, and all actions can be scoped per user.
|
||||
|
||||
- Upload/download with instant feedback
|
||||
- Create and organize files in folders
|
||||
- File previews, type validation, and size limits
|
||||
- Token-based sharing system
|
||||
- Disk usage tracking by user
|
@@ -5,7 +5,7 @@ icon: Cog
|
||||
|
||||
Hey there! Looking to run **Palmr.** your way, with complete control over every piece of the stack? This manual installation guide is for you. No Docker, no pre-built containers just the raw source code to tweak, customize, and deploy as you see fit.
|
||||
|
||||
> **Prefer a quicker setup?** If this hands-on approach feels like overkill, check out our [**Quick Start (Docker)**](/docs/3.1-beta/quick-start) guide for a fast, containerized deployment. This manual path is tailored for developers who want to dive deep, modify the codebase, or integrate custom services.
|
||||
> **Prefer a quicker setup?** If this hands-on approach feels like overkill, check out our [**Quick Start (Docker)**](/docs/3.2-beta/quick-start) guide for a fast, containerized deployment. This manual path is tailored for developers who want to dive deep, modify the codebase, or integrate custom services.
|
||||
|
||||
Here's what you'll do at a glance:
|
||||
|
||||
@@ -165,6 +165,27 @@ cp .env.example .env
|
||||
|
||||
This creates a `.env` file with the necessary configurations for the frontend.
|
||||
|
||||
##### Upload Configuration
|
||||
|
||||
Palmr. supports configurable chunked uploading for large files. You can customize the chunk size by setting the following environment variable in your `.env` file:
|
||||
|
||||
```bash
|
||||
NEXT_PUBLIC_UPLOAD_CHUNK_SIZE_MB=100
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
- If `NEXT_PUBLIC_UPLOAD_CHUNK_SIZE_MB` is set, Palmr. will use this value (in megabytes) as the chunk size for all file uploads that exceed this threshold.
|
||||
- If not set or left empty, Palmr. automatically calculates optimal chunk sizes based on file size:
|
||||
- Files ≤ 100MB: uploaded without chunking
|
||||
- Files > 100MB and ≤ 1GB: 75MB chunks
|
||||
- Files > 1GB: 150MB chunks
|
||||
|
||||
**When to configure:**
|
||||
|
||||
- **Default (not set):** Recommended for most use cases. Palmr. will intelligently determine the best chunk size.
|
||||
- **Custom value:** Set this if you have specific network conditions or want to optimize for your infrastructure (e.g., slower connections may benefit from smaller chunks like 50MB, while fast networks can handle larger chunks like 200MB, or the upload size per payload may be limited by a proxy like Cloudflare)
|
||||
|
||||
#### Install dependencies
|
||||
|
||||
Install all the frontend dependencies:
|
||||
@@ -201,6 +222,17 @@ You should see the full Palmr. application ready to go!
|
||||
|
||||
This guide sets up Palmr. using the local file system for storage. Want to use an S3-compatible object storage instead? You can configure that in the `.env` file. Check the Palmr. documentation for details on setting up S3 storage just update the environment variables, then build and run as shown here.
|
||||
|
||||
### Custom Installation Paths and Symlinks
|
||||
|
||||
If you're using a custom installation setup with symlinks (for example, `/opt/palmr_data/uploads -> /mnt/data/uploads`), you might encounter issues with disk space detection. Palmr. includes a `CUSTOM_PATH` environment variable to handle these scenarios:
|
||||
|
||||
```bash
|
||||
# In your .env file (apps/server/.env)
|
||||
CUSTOM_PATH=/opt/palmr_data
|
||||
```
|
||||
|
||||
This tells Palmr. to check your custom path first when determining available disk space, ensuring proper detection even when using symlinks or non-standard directory structures.
|
||||
|
||||
---
|
||||
|
||||
## Command cheat sheet
|
||||
@@ -232,10 +264,10 @@ pnpm serve
|
||||
|
||||
Palmr. is now up and running locally . Here are some suggested next steps:
|
||||
|
||||
- **Manage Users**: Dive into the [Users Management](/docs/3.1-beta/manage-users) guide.
|
||||
- **Manage Users**: Dive into the [Users Management](/docs/3.2-beta/manage-users) guide.
|
||||
- **Switch to Object Storage**: Update `.env` variables to use an S3-compatible bucket (see Quick Notes above).
|
||||
- **Secure Your Instance**: Put Palmr. behind a reverse proxy like **Nginx** or **Caddy** and enable HTTPS.
|
||||
- **Learn the Internals**: Explore how everything connects in the [Architecture](/docs/3.1-beta/architecture) overview.
|
||||
- **Learn the Internals**: Explore how everything connects in the [Architecture](/docs/3.2-beta/architecture) overview.
|
||||
|
||||
Jump into whichever area fits your needs our docs are designed for exploration in any order.
|
||||
|
@@ -14,6 +14,7 @@
|
||||
"available-languages",
|
||||
"uid-gid-configuration",
|
||||
"reverse-proxy-configuration",
|
||||
"download-memory-management",
|
||||
"password-reset-without-smtp",
|
||||
"oidc-authentication",
|
||||
"troubleshooting",
|
||||
@@ -29,5 +30,5 @@
|
||||
"gh-sponsor"
|
||||
],
|
||||
"root": true,
|
||||
"title": "v3.1-beta"
|
||||
"title": "v3.2-beta"
|
||||
}
|
@@ -360,7 +360,7 @@ With Auth0 authentication configured, you might want to:
|
||||
- **Review security settings**: Ensure your authentication setup meets your security requirements
|
||||
- **Monitor usage**: Keep track of authentication patterns and user activity
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
@@ -374,7 +374,7 @@ With Authentik authentication configured, you might want to:
|
||||
- **Review security settings**: Ensure your authentication setup meets your security requirements
|
||||
- **Monitor usage**: Keep track of authentication patterns and user activity
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
@@ -332,7 +332,7 @@ With Discord authentication configured, you might want to:
|
||||
- **Review security settings**: Ensure your authentication setup meets your security requirements
|
||||
- **Monitor usage**: Keep track of authentication patterns and user activity
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
@@ -314,7 +314,7 @@ With Frontegg authentication configured, you might want to:
|
||||
- **Review security settings**: Ensure your authentication setup meets your security requirements
|
||||
- **Monitor usage**: Keep track of authentication patterns and user activity
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
@@ -295,7 +295,7 @@ With GitHub authentication configured, you might want to:
|
||||
- **Review security settings**: Ensure your authentication setup meets your security requirements
|
||||
- **Monitor usage**: Keep track of authentication patterns and user activity
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
@@ -325,7 +325,7 @@ With Google authentication configured, you might want to:
|
||||
- **Review security settings**: Ensure your authentication setup meets your security requirements
|
||||
- **Monitor usage**: Keep track of authentication patterns and user activity
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
@@ -38,14 +38,14 @@ Before configuring OIDC authentication, ensure you have:
|
||||
|
||||
Palmr's OIDC implementation is compatible with any OpenID Connect compliant provider, including as official providers:
|
||||
|
||||
- **[Google](/docs/3.1-beta/oidc-authentication/google)**
|
||||
- **[Discord](/docs/3.1-beta/oidc-authentication/discord)**
|
||||
- **[Github](/docs/3.1-beta/oidc-authentication/github)**
|
||||
- **[Zitadel](/docs/3.1-beta/oidc-authentication/zitadel)**
|
||||
- **[Auth0](/docs/3.1-beta/oidc-authentication/auth0)**
|
||||
- **[Authentik](/docs/3.1-beta/oidc-authentication/authentik)**
|
||||
- **[Frontegg](/docs/3.1-beta/oidc-authentication/frontegg)**
|
||||
- **[Kinde Auth](/docs/3.1-beta/oidc-authentication/kinde-auth)**
|
||||
- **[Google](/docs/3.2-beta/oidc-authentication/google)**
|
||||
- **[Discord](/docs/3.2-beta/oidc-authentication/discord)**
|
||||
- **[Github](/docs/3.2-beta/oidc-authentication/github)**
|
||||
- **[Zitadel](/docs/3.2-beta/oidc-authentication/zitadel)**
|
||||
- **[Auth0](/docs/3.2-beta/oidc-authentication/auth0)**
|
||||
- **[Authentik](/docs/3.2-beta/oidc-authentication/authentik)**
|
||||
- **[Frontegg](/docs/3.2-beta/oidc-authentication/frontegg)**
|
||||
- **[Kinde Auth](/docs/3.2-beta/oidc-authentication/kinde-auth)**
|
||||
|
||||
Although these are the official providers (internally tested with 100% success), you can connect any OIDC provider by providing your credentials and connection URL. We've developed a practical way to integrate virtually all OIDC providers available in the market. In this documentation, you can consult how to configure each of the official providers, as well as include other providers not listed as official. Just below, you will find instructions on how to access the OIDC provider configuration. For specific details about configuring each provider, select the desired option in the sidebar, in the "OIDC Authentication" section.
|
||||
|
@@ -359,7 +359,7 @@ With Kinde Auth authentication configured, you might want to:
|
||||
- **Review security settings**: Ensure your authentication setup meets your security requirements
|
||||
- **Monitor usage**: Keep track of authentication patterns and user activity
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
@@ -15,4 +15,4 @@
|
||||
"other"
|
||||
],
|
||||
"title": "OIDC Authentication"
|
||||
}
|
||||
}
|
@@ -270,10 +270,10 @@ After configuring Pocket ID authentication:
|
||||
- **User management**: Review auto-registration settings
|
||||
- **Backup verification**: Test backup and restore procedures
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
||||
- [Pocket ID Documentation](https://docs.pocket-id.org)
|
||||
- [OIDC Specification](https://openid.net/specs/openid-connect-core-1_0.html)
|
||||
- [Palmr OIDC Overview](/docs/3.1-beta/oidc-authentication)
|
||||
- [Palmr OIDC Overview](/docs/3.2-beta/oidc-authentication)
|
@@ -413,7 +413,7 @@ With Zitadel authentication configured, you might want to:
|
||||
- **Review security settings**: Ensure your authentication setup meets your security requirements
|
||||
- **Monitor usage**: Keep track of authentication patterns and user activity
|
||||
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.1-beta/oidc-authentication).
|
||||
For more information about OIDC authentication in Palmr, see the [OIDC Authentication overview](/docs/3.2-beta/oidc-authentication).
|
||||
|
||||
## Useful resources
|
||||
|
@@ -69,6 +69,14 @@ Choose your storage method based on your needs:
|
||||
# - PALMR_GID=1000 # GID for the container processes (default is 1000)
|
||||
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
# - DEFAULT_LANGUAGE=en-US # Default language for the application (optional, defaults to en-US)
|
||||
# - PRESIGNED_URL_EXPIRATION=3600 # Duration in seconds for presigned URL expiration (optional, defaults to 3600 seconds / 1 hour)
|
||||
# - DOWNLOAD_MAX_CONCURRENT=5 # Maximum simultaneous downloads (auto-scales if not set)
|
||||
# - DOWNLOAD_MEMORY_THRESHOLD_MB=2048 # Memory threshold in MB before throttling (auto-scales if not set)
|
||||
# - DOWNLOAD_QUEUE_SIZE=25 # Maximum queue size for pending downloads (auto-scales if not set)
|
||||
# - DOWNLOAD_MIN_FILE_SIZE_GB=3.0 # Minimum file size in GB to activate memory management (default: 3.0)
|
||||
# - DOWNLOAD_AUTO_SCALE=true # Enable auto-scaling based on system memory (default: true)
|
||||
# - NODE_OPTIONS=--expose-gc # Enable garbage collection for large downloads (recommended for production)
|
||||
# - NEXT_PUBLIC_UPLOAD_CHUNK_SIZE_MB=100 # Chunk size in MB for large file uploads (OPTIONAL - auto-calculates if not set)
|
||||
volumes:
|
||||
- palmr_data:/app/server
|
||||
|
||||
@@ -77,7 +85,7 @@ Choose your storage method based on your needs:
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
**Having upload or permission issues?** Add `PALMR_UID=1000` and `PALMR_GID=1000` to your environment variables. Check our [UID/GID Configuration](/docs/3.1-beta/uid-gid-configuration) guide for more details.
|
||||
**Having upload or permission issues?** Add `PALMR_UID=1000` and `PALMR_GID=1000` to your environment variables. Check our [UID/GID Configuration](/docs/3.2-beta/uid-gid-configuration) guide for more details.
|
||||
</Callout>
|
||||
|
||||
### Deploy
|
||||
@@ -106,7 +114,7 @@ Choose your storage method based on your needs:
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5487:5487" # Web interface
|
||||
# - "3333:3333" # API (optional)
|
||||
# - "3333:3333" # API (optional)
|
||||
environment:
|
||||
# Optional: Uncomment and configure as needed (if you don`t use, you can remove)
|
||||
# - ENABLE_S3=true # Set to true to enable S3-compatible storage
|
||||
@@ -116,12 +124,19 @@ Choose your storage method based on your needs:
|
||||
# - PALMR_GID=1000 # GID for the container processes (default is 1000)
|
||||
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
|
||||
# - DEFAULT_LANGUAGE=en-US # Default language for the application (optional, defaults to en-US)
|
||||
# - PRESIGNED_URL_EXPIRATION=3600 # Duration in seconds for presigned URL expiration (optional, defaults to 3600 seconds / 1 hour)
|
||||
# - DOWNLOAD_MAX_CONCURRENT=5 # Maximum simultaneous downloads (auto-scales if not set)
|
||||
# - DOWNLOAD_MEMORY_THRESHOLD_MB=2048 # Memory threshold in MB before throttling (auto-scales if not set)
|
||||
# - DOWNLOAD_QUEUE_SIZE=25 # Maximum queue size for pending downloads (auto-scales if not set)
|
||||
# - DOWNLOAD_MIN_FILE_SIZE_GB=3.0 # Minimum file size in GB to activate memory management (default: 3.0)
|
||||
# - DOWNLOAD_AUTO_SCALE=true # Enable auto-scaling based on system memory (default: true)
|
||||
# - NODE_OPTIONS=--expose-gc # Enable garbage collection for large downloads (recommended for production)
|
||||
volumes:
|
||||
- ./data:/app/server
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
**Having upload or permission issues?** Add `PALMR_UID=1000` and `PALMR_GID=1000` to your environment variables. Check our [UID/GID Configuration](/docs/3.1-beta/uid-gid-configuration) guide for more details.
|
||||
**Having upload or permission issues?** Add `PALMR_UID=1000` and `PALMR_GID=1000` to your environment variables. Check our [UID/GID Configuration](/docs/3.2-beta/uid-gid-configuration) guide for more details.
|
||||
</Callout>
|
||||
|
||||
### Deploy
|
||||
@@ -137,15 +152,33 @@ Choose your storage method based on your needs:
|
||||
|
||||
Customize Palmr's behavior with these environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------- | ------- | -------------------------------------------------------------------------------------------- |
|
||||
| `ENABLE_S3` | `false` | Enable S3-compatible storage backends |
|
||||
| `ENCRYPTION_KEY` | - | **Required when encryption is enabled**: 32+ character key for file encryption |
|
||||
| `DISABLE_FILESYSTEM_ENCRYPTION` | `true` | Disable file encryption for better performance (set to `false` to enable encryption) |
|
||||
| `SECURE_SITE` | `false` | Enable secure cookies for HTTPS/reverse proxy deployments |
|
||||
| `DEFAULT_LANGUAGE` | `en-US` | Default application language ([see available languages](/docs/3.1-beta/available-languages)) |
|
||||
| `PALMR_UID` | `1000` | User ID for container processes (helps with file permissions) |
|
||||
| `PALMR_GID` | `1000` | Group ID for container processes (helps with file permissions) |
|
||||
| Variable | Default | Description |
|
||||
| ---------------------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `ENABLE_S3` | `false` | Enable S3-compatible storage backends |
|
||||
| `S3_ENDPOINT` | - | S3 server endpoint URL (required when using S3) |
|
||||
| `S3_PORT` | - | S3 server port (optional when using S3) |
|
||||
| `S3_USE_SSL` | - | Enable SSL for S3 connections (optional when using S3) |
|
||||
| `S3_ACCESS_KEY` | - | S3 access key for authentication (required when using S3) |
|
||||
| `S3_SECRET_KEY` | - | S3 secret key for authentication (required when using S3) |
|
||||
| `S3_REGION` | - | S3 region configuration (optional when using S3) |
|
||||
| `S3_BUCKET_NAME` | - | S3 bucket name for file storage (required when using S3) |
|
||||
| `S3_FORCE_PATH_STYLE` | `false` | Force path-style S3 URLs (optional when using S3) |
|
||||
| `S3_REJECT_UNAUTHORIZED` | `true` | Enable strict SSL certificate validation for S3 (set to `false` for self-signed certificates) |
|
||||
| `ENCRYPTION_KEY` | - | **Required when encryption is enabled**: 32+ character key for file encryption |
|
||||
| `DISABLE_FILESYSTEM_ENCRYPTION` | `true` | Disable file encryption for better performance (set to `false` to enable encryption) |
|
||||
| `PRESIGNED_URL_EXPIRATION` | `3600` | Duration in seconds for presigned URL expiration (applies to both filesystem and S3 storage) |
|
||||
| `CUSTOM_PATH` | - | Custom base path for disk space detection in manual installations with symlinks |
|
||||
| `SECURE_SITE` | `false` | Enable secure cookies for HTTPS/reverse proxy deployments |
|
||||
| `DEFAULT_LANGUAGE` | `en-US` | Default application language ([see available languages](/docs/3.2-beta/available-languages)) |
|
||||
| `PALMR_UID` | `1000` | User ID for container processes (helps with file permissions) |
|
||||
| `PALMR_GID` | `1000` | Group ID for container processes (helps with file permissions) |
|
||||
| `NODE_OPTIONS` | - | Node.js options (recommended: `--expose-gc` for garbage collection in production) |
|
||||
| `DOWNLOAD_MAX_CONCURRENT` | auto-scale | Maximum number of simultaneous downloads (see [Download Memory Management](/docs/3.2-beta/download-memory-management)) |
|
||||
| `DOWNLOAD_MEMORY_THRESHOLD_MB` | auto-scale | Memory threshold in MB before throttling |
|
||||
| `DOWNLOAD_QUEUE_SIZE` | auto-scale | Maximum queue size for pending downloads |
|
||||
| `DOWNLOAD_MIN_FILE_SIZE_GB` | `3.0` | Minimum file size in GB to activate memory management |
|
||||
| `NEXT_PUBLIC_UPLOAD_CHUNK_SIZE_MB` | auto-calculate | Chunk size in MB for large file uploads (see [Chunked Upload Configuration](/docs/3.2-beta/quick-start#chunked-upload-configuration)) |
|
||||
| `DOWNLOAD_AUTO_SCALE` | `true` | Enable auto-scaling based on system memory |
|
||||
|
||||
<Callout type="info">
|
||||
**Performance First**: Palmr runs without encryption by default for optimal speed and lower resource usage—perfect for
|
||||
@@ -160,7 +193,7 @@ Customize Palmr's behavior with these environment variables:
|
||||
|
||||
<Callout>
|
||||
**Using a Reverse Proxy?** Set `SECURE_SITE=true` and check our [Reverse Proxy
|
||||
Configuration](/docs/3.1-beta/reverse-proxy-configuration) guide for proper HTTPS setup.
|
||||
Configuration](/docs/3.2-beta/reverse-proxy-configuration) guide for proper HTTPS setup.
|
||||
</Callout>
|
||||
|
||||
### Generate Encryption Keys (Optional)
|
||||
@@ -180,7 +213,7 @@ Once deployed, open Palmr in your browser:
|
||||
|
||||
<Callout type="info">
|
||||
**Learn More**: For complete API documentation, authentication, and integration examples, see our [API
|
||||
Reference](/docs/3.1-beta/api) guide
|
||||
Reference](/docs/3.2-beta/api) guide
|
||||
</Callout>
|
||||
|
||||
<Callout type="warn">
|
||||
@@ -216,7 +249,7 @@ Prefer Docker commands over Compose? Here are the equivalent commands:
|
||||
|
||||
|
||||
<Callout type="info">
|
||||
**Permission Issues?** Add `-e PALMR_UID=1000 -e PALMR_GID=1000` to the command above. See our [UID/GID Configuration](/docs/3.1-beta/uid-gid-configuration) guide for details.
|
||||
**Permission Issues?** Add `-e PALMR_UID=1000 -e PALMR_GID=1000` to the command above. See our [UID/GID Configuration](/docs/3.2-beta/uid-gid-configuration) guide for details.
|
||||
</Callout>
|
||||
|
||||
</Tab>
|
||||
@@ -242,7 +275,7 @@ Prefer Docker commands over Compose? Here are the equivalent commands:
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
**Permission Issues?** Add `-e PALMR_UID=1000 -e PALMR_GID=1000` to the command above. See our [UID/GID Configuration](/docs/3.1-beta/uid-gid-configuration) guide for details.
|
||||
**Permission Issues?** Add `-e PALMR_UID=1000 -e PALMR_GID=1000` to the command above. See our [UID/GID Configuration](/docs/3.2-beta/uid-gid-configuration) guide for details.
|
||||
</Callout>
|
||||
|
||||
</Tab>
|
||||
@@ -250,6 +283,63 @@ Prefer Docker commands over Compose? Here are the equivalent commands:
|
||||
|
||||
---
|
||||
|
||||
## Common Configuration Options
|
||||
|
||||
### Presigned URL Expiration
|
||||
|
||||
Palmr. uses temporary URLs (presigned URLs) for secure file access. These URLs expire after a configurable time period to enhance security.
|
||||
|
||||
**Default:** 1 hour (3600 seconds)
|
||||
|
||||
You can customize this for all storage types (filesystem or S3) by adding:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- PRESIGNED_URL_EXPIRATION=7200 # 2 hours
|
||||
```
|
||||
|
||||
**When to adjust:**
|
||||
|
||||
- **Shorter time (1800 = 30 min):** Higher security, but users may need to refresh download links
|
||||
- **Longer time (7200-21600 = 2-6 hours):** Better for large file transfers, but URLs stay valid longer
|
||||
- **Default (3600 = 1 hour):** Good balance for most use cases
|
||||
|
||||
### File Encryption
|
||||
|
||||
For filesystem storage, you can enable file encryption:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- DISABLE_FILESYSTEM_ENCRYPTION=false
|
||||
- ENCRYPTION_KEY=your-secure-32-character-key-here
|
||||
```
|
||||
|
||||
**Note:** S3 storage handles encryption through your S3 provider's encryption features.
|
||||
|
||||
### Chunked Upload Configuration
|
||||
|
||||
Palmr supports configurable chunked uploading for large files. You can customize the chunk size by setting the following environment variable:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- NEXT_PUBLIC_UPLOAD_CHUNK_SIZE_MB=100 # Chunk size in MB
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
|
||||
- If `NEXT_PUBLIC_UPLOAD_CHUNK_SIZE_MB` is set, Palmr will use this value (in megabytes) as the chunk size for all file uploads that exceed this threshold.
|
||||
- If not set or left empty, Palmr automatically calculates optimal chunk sizes based on file size:
|
||||
- Files ≤ 100MB: uploaded without chunking
|
||||
- Files > 100MB and ≤ 1GB: 75MB chunks
|
||||
- Files > 1GB: 150MB chunks
|
||||
|
||||
**When to configure:**
|
||||
|
||||
- **Default (not set):** Recommended for most use cases. Palmr will intelligently determine the best chunk size.
|
||||
- **Custom value:** Set this if you have specific network conditions or want to optimize for your infrastructure (e.g., slower connections may benefit from smaller chunks like 50MB, while fast networks can handle larger chunks like 200MB, or the upload size per payload may be limited by a proxy like Cloudflare)
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Updates
|
||||
@@ -301,16 +391,17 @@ Your Palmr instance is ready! Here's what you can explore:
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
- **[UID/GID Configuration](/docs/3.1-beta/uid-gid-configuration)** - Configure user permissions for NAS systems and custom environments
|
||||
- **[S3 Storage](/docs/3.1-beta/s3-configuration)** - Scale with Amazon S3 or compatible storage providers
|
||||
- **[Manual Installation](/docs/3.1-beta/manual-installation)** - Manual installation and custom configurations
|
||||
- **[UID/GID Configuration](/docs/3.2-beta/uid-gid-configuration)** - Configure user permissions for NAS systems and custom environments
|
||||
- **[Download Memory Management](/docs/3.2-beta/download-memory-management)** - Configure large file download handling and queue system
|
||||
- **[S3 Storage](/docs/3.2-beta/s3-configuration)** - Scale with Amazon S3 or compatible storage providers
|
||||
- **[Manual Installation](/docs/3.2-beta/manual-installation)** - Manual installation and custom configurations
|
||||
|
||||
### Integration & Development
|
||||
|
||||
- **[API Reference](/docs/3.1-beta/api)** - Integrate Palmr. with your applications
|
||||
- **[API Reference](/docs/3.2-beta/api)** - Integrate Palmr. with your applications
|
||||
|
||||
<Callout type="info">
|
||||
**Need help?** Check our [Troubleshooting Guide](/docs/3.1-beta/troubleshooting) for common issues and solutions.
|
||||
**Need help?** Check our [Troubleshooting Guide](/docs/3.2-beta/troubleshooting) for common issues and solutions.
|
||||
</Callout>
|
||||
|
||||
---
|
@@ -131,7 +131,7 @@ environment:
|
||||
# - ENCRYPTION_KEY=your-key-here # Required only if encryption is enabled
|
||||
```
|
||||
|
||||
> **💡 Note**: Check your host UID/GID with `id` command and use those values. See [UID/GID Configuration](/docs/3.1-beta/uid-gid-configuration) for detailed setup.
|
||||
> **💡 Note**: Check your host UID/GID with `id` command and use those values. See [UID/GID Configuration](/docs/3.2-beta/uid-gid-configuration) for detailed setup.
|
||||
|
||||
---
|
||||
|
@@ -7,6 +7,8 @@ This guide provides comprehensive configuration instructions for integrating Pal
|
||||
|
||||
> **Overview:** Palmr. supports any S3-compatible storage provider, giving you flexibility to choose the solution that best fits your needs and budget.
|
||||
|
||||
> **Note:** Some configuration options (like presigned URL expiration) apply to **all storage types**, including filesystem storage. These are marked accordingly in the documentation.
|
||||
|
||||
## When to use S3-compatible storage
|
||||
|
||||
Consider using S3-compatible storage when you need:
|
||||
@@ -19,18 +21,27 @@ Consider using S3-compatible storage when you need:
|
||||
|
||||
## Environment variables
|
||||
|
||||
### General configuration (applies to all storage types)
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| -------------------------- | ------------------------------------------------ | -------- | --------------- |
|
||||
| `PRESIGNED_URL_EXPIRATION` | Duration in seconds for presigned URL expiration | No | `3600` (1 hour) |
|
||||
|
||||
### S3-specific configuration
|
||||
|
||||
To enable S3-compatible storage, set `ENABLE_S3=true` and configure the following environment variables:
|
||||
|
||||
| Variable | Description | Required | Default |
|
||||
| --------------------- | ----------------------------- | -------- | ----------------- |
|
||||
| `S3_ENDPOINT` | S3 provider endpoint URL | Yes | - |
|
||||
| `S3_PORT` | Connection port | No | Based on protocol |
|
||||
| `S3_USE_SSL` | Enable SSL/TLS encryption | Yes | `true` |
|
||||
| `S3_ACCESS_KEY` | Access key for authentication | Yes | - |
|
||||
| `S3_SECRET_KEY` | Secret key for authentication | Yes | - |
|
||||
| `S3_REGION` | Storage region | Yes | - |
|
||||
| `S3_BUCKET_NAME` | Bucket/container name | Yes | - |
|
||||
| `S3_FORCE_PATH_STYLE` | Use path-style URLs | No | `false` |
|
||||
| Variable | Description | Required | Default |
|
||||
| ------------------------ | ---------------------------------------- | -------- | ----------------- |
|
||||
| `S3_ENDPOINT` | S3 provider endpoint URL | Yes | - |
|
||||
| `S3_PORT` | Connection port | No | Based on protocol |
|
||||
| `S3_USE_SSL` | Enable SSL/TLS encryption | Yes | `true` |
|
||||
| `S3_ACCESS_KEY` | Access key for authentication | Yes | - |
|
||||
| `S3_SECRET_KEY` | Secret key for authentication | Yes | - |
|
||||
| `S3_REGION` | Storage region | Yes | - |
|
||||
| `S3_BUCKET_NAME` | Bucket/container name | Yes | - |
|
||||
| `S3_FORCE_PATH_STYLE` | Use path-style URLs | No | `false` |
|
||||
| `S3_REJECT_UNAUTHORIZED` | Enable strict SSL certificate validation | No | `true` |
|
||||
|
||||
## Provider configurations
|
||||
|
||||
@@ -51,6 +62,7 @@ S3_SECRET_KEY=your-secret-access-key
|
||||
S3_REGION=us-east-1
|
||||
S3_BUCKET_NAME=your-bucket-name
|
||||
S3_FORCE_PATH_STYLE=false
|
||||
# PRESIGNED_URL_EXPIRATION=3600 # Optional: 1 hour (default)
|
||||
```
|
||||
|
||||
**Getting credentials:**
|
||||
@@ -81,6 +93,21 @@ S3_FORCE_PATH_STYLE=true
|
||||
- Default MinIO port is 9000
|
||||
- SSL can be disabled for local development
|
||||
|
||||
**For MinIO with self-signed SSL certificates:**
|
||||
|
||||
```bash
|
||||
ENABLE_S3=true
|
||||
S3_ENDPOINT=your-minio-domain.com
|
||||
S3_PORT=9000
|
||||
S3_USE_SSL=true
|
||||
S3_ACCESS_KEY=your-minio-access-key
|
||||
S3_SECRET_KEY=your-minio-secret-key
|
||||
S3_REGION=us-east-1
|
||||
S3_BUCKET_NAME=your-bucket-name
|
||||
S3_FORCE_PATH_STYLE=true
|
||||
S3_REJECT_UNAUTHORIZED=false # Allows self-signed certificates
|
||||
```
|
||||
|
||||
### Google Cloud Storage
|
||||
|
||||
Google Cloud Storage offers competitive pricing and global infrastructure.
|
||||
@@ -137,6 +164,7 @@ S3_SECRET_KEY=your-application-key
|
||||
S3_REGION=us-west-002
|
||||
S3_BUCKET_NAME=your-bucket-name
|
||||
S3_FORCE_PATH_STYLE=false
|
||||
# PRESIGNED_URL_EXPIRATION=7200 # Optional: 2 hours for large files
|
||||
```
|
||||
|
||||
**Cost advantage:**
|
||||
@@ -187,6 +215,93 @@ S3_FORCE_PATH_STYLE=false
|
||||
- Use container name as bucket name
|
||||
- Configure appropriate access policies
|
||||
|
||||
## Presigned URL configuration
|
||||
|
||||
Palmr. uses presigned URLs to provide secure, temporary access to files stored in **both S3-compatible storage and filesystem storage**. These URLs have a configurable expiration time to balance security and usability.
|
||||
|
||||
> **Note:** This configuration applies to **all storage types** (S3, filesystem, etc.), not just S3-compatible storage.
|
||||
|
||||
### Understanding presigned URLs
|
||||
|
||||
Presigned URLs are temporary URLs that allow direct access to files without exposing storage credentials or requiring authentication. They automatically expire after a specified time period, enhancing security by limiting access duration.
|
||||
|
||||
**How it works:**
|
||||
|
||||
- **S3 Storage:** URLs are signed by AWS/S3-compatible provider credentials
|
||||
- **Filesystem Storage:** URLs use temporary tokens that are validated by Palmr server
|
||||
|
||||
**Default behavior:**
|
||||
|
||||
- Upload URLs: 1 hour (3600 seconds)
|
||||
- Download URLs: 1 hour (3600 seconds)
|
||||
|
||||
### Configuring expiration time
|
||||
|
||||
You can customize the expiration time using the `PRESIGNED_URL_EXPIRATION` environment variable:
|
||||
|
||||
```bash
|
||||
# Set URLs to expire after 2 hours (7200 seconds)
|
||||
PRESIGNED_URL_EXPIRATION=7200
|
||||
|
||||
# Set URLs to expire after 30 minutes (1800 seconds)
|
||||
PRESIGNED_URL_EXPIRATION=1800
|
||||
|
||||
# Set URLs to expire after 6 hours (21600 seconds)
|
||||
PRESIGNED_URL_EXPIRATION=21600
|
||||
```
|
||||
|
||||
### Choosing the right expiration time
|
||||
|
||||
**Shorter expiration (15-30 minutes):**
|
||||
|
||||
- [+] Higher security
|
||||
- [+] Reduced risk of unauthorized access
|
||||
- [-] May interrupt long uploads/downloads
|
||||
- [-] Users may need to refresh links more often
|
||||
|
||||
**Longer expiration (2-6 hours):**
|
||||
|
||||
- [+] Better user experience for large files
|
||||
- [+] Fewer interruptions during transfers
|
||||
- [-] Longer exposure window if URLs are compromised
|
||||
- [-] Potential for increased storage costs if users leave downloads incomplete
|
||||
|
||||
**Recommended settings:**
|
||||
|
||||
- **High security environments:** 1800 seconds (30 minutes)
|
||||
- **Standard usage:** 3600 seconds (1 hour) - default
|
||||
- **Large file transfers:** 7200-21600 seconds (2-6 hours)
|
||||
|
||||
### Example configurations
|
||||
|
||||
**For Backblaze B2 with extended expiration:**
|
||||
|
||||
```bash
|
||||
ENABLE_S3=true
|
||||
S3_ENDPOINT=s3.us-west-002.backblazeb2.com
|
||||
S3_USE_SSL=true
|
||||
S3_ACCESS_KEY=your-key-id
|
||||
S3_SECRET_KEY=your-application-key
|
||||
S3_REGION=us-west-002
|
||||
S3_BUCKET_NAME=your-bucket-name
|
||||
S3_FORCE_PATH_STYLE=false
|
||||
PRESIGNED_URL_EXPIRATION=7200 # 2 hours for large file transfers
|
||||
```
|
||||
|
||||
**For high-security environments:**
|
||||
|
||||
```bash
|
||||
ENABLE_S3=true
|
||||
S3_ENDPOINT=s3.amazonaws.com
|
||||
S3_USE_SSL=true
|
||||
S3_ACCESS_KEY=your-access-key-id
|
||||
S3_SECRET_KEY=your-secret-access-key
|
||||
S3_REGION=us-east-1
|
||||
S3_BUCKET_NAME=your-bucket-name
|
||||
S3_FORCE_PATH_STYLE=false
|
||||
PRESIGNED_URL_EXPIRATION=1800 # 30 minutes for enhanced security
|
||||
```
|
||||
|
||||
## Configuration best practices
|
||||
|
||||
### Security considerations
|
||||
@@ -212,6 +327,19 @@ S3_FORCE_PATH_STYLE=false
|
||||
- Check firewall and network connectivity
|
||||
- Ensure SSL/TLS settings match provider requirements
|
||||
|
||||
**SSL certificate errors (self-signed certificates):**
|
||||
|
||||
If you encounter errors like `unable to verify the first certificate` or `UNABLE_TO_VERIFY_LEAF_SIGNATURE`, you're likely using self-signed SSL certificates. This is common with self-hosted MinIO or other S3-compatible services.
|
||||
|
||||
**Solution:**
|
||||
Set `S3_REJECT_UNAUTHORIZED=false` in your environment variables to allow self-signed certificates:
|
||||
|
||||
```bash
|
||||
S3_REJECT_UNAUTHORIZED=false
|
||||
```
|
||||
|
||||
**Note:** SSL certificate validation is enabled by default (`true`) for security. Set it to `false` only when using self-hosted S3 services with self-signed certificates.
|
||||
|
||||
**Authentication failures:**
|
||||
|
||||
- Confirm access key and secret key are correct
|
@@ -44,7 +44,7 @@ The central hub after login, providing an overview of recent activity, quick act
|
||||
|
||||
### Files list view
|
||||
|
||||
Comprehensive file browser displaying all uploaded files in a detailed list format with metadata, actions, and sorting options.
|
||||
Comprehensive file browser displaying all uploaded files in a detailed list format with metadata, actions, sorting options, and folder navigation.
|
||||
|
||||
<ZoomableImage
|
||||
src="/assets/v3/screenshots/files-list.png"
|
||||
@@ -53,7 +53,7 @@ Comprehensive file browser displaying all uploaded files in a detailed list form
|
||||
|
||||
### Files card view
|
||||
|
||||
Alternative file browser layout showing files as visual cards, perfect for quick browsing and visual file identification.
|
||||
Alternative file browser layout showing files as visual cards, perfect for quick browsing, visual file identification, and folder navigation.
|
||||
|
||||
<ZoomableImage
|
||||
src="/assets/v3/screenshots/files-card.png"
|
||||
@@ -73,7 +73,7 @@ File upload interface where users can drag and drop or select files to upload to
|
||||
|
||||
### Shares page
|
||||
|
||||
Management interface for all shared files and folders, showing share status, permissions, and access controls.
|
||||
Management interface for all shared files and folders, showing share status, permissions, and access controls for both individual files and folders.
|
||||
|
||||
<ZoomableImage
|
||||
src="/assets/v3/screenshots/shares.png"
|
@@ -366,13 +366,11 @@ If none of these solutions work:
|
||||
```
|
||||
|
||||
2. **Check our documentation:**
|
||||
|
||||
- [UID/GID Configuration](/docs/3.0-beta/uid-gid-configuration)
|
||||
- [Quick Start Guide](/docs/3.0-beta/quick-start)
|
||||
- [API Reference](/docs/3.0-beta/api)
|
||||
|
||||
3. **Open an issue on GitHub:**
|
||||
|
||||
- Include your `docker-compose.yaml`
|
||||
- Include relevant log output
|
||||
- Describe your system (OS, Docker version, etc.)
|
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"pages": ["3.1-beta", "2.0.0-beta"]
|
||||
"pages": ["3.2-beta", "2.0.0-beta"]
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-docs",
|
||||
"version": "3.1.8-beta",
|
||||
"version": "3.2.3-beta",
|
||||
"description": "Docs for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
|
4878
apps/docs/pnpm-lock.yaml
generated
4878
apps/docs/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -59,13 +59,13 @@ const images = [
|
||||
"https://res.cloudinary.com/technical-intelligence/image/upload/v1745546005/Palmr./profile_mizwvg.png",
|
||||
];
|
||||
|
||||
const docsLink = "/docs/3.1-beta";
|
||||
const docsLink = "/docs/3.2-beta";
|
||||
|
||||
function Hero() {
|
||||
return (
|
||||
<section className="relative z-[2] flex flex-col border-x border-t px-6 pt-12 pb-10 md:px-12 md:pt-16 max-md:text-center">
|
||||
<h1 className="mb-8 text-6xl font-bold">
|
||||
Palmr. <span className="text-[13px] font-light text-muted-foreground/50 font-mono">v3.1-beta</span>
|
||||
Palmr. <span className="text-[13px] font-light text-muted-foreground/50 font-mono">v3.2-beta</span>
|
||||
</h1>
|
||||
<h1 className="hidden text-4xl font-medium max-w-[600px] md:block mb-4">Modern & efficient file sharing</h1>
|
||||
<p className="mb-8 text-fd-muted-foreground md:max-w-[80%] md:text-xl">
|
||||
@@ -294,7 +294,7 @@ function FullWidthFooter() {
|
||||
<div className="flex items-center gap-1 text-sm max-w-7xl">
|
||||
<span>Powered by</span>
|
||||
<Link
|
||||
href="http://kyantech.com.br"
|
||||
href="https://github.com/kyantech"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="flex items-center hover:text-green-700 text-green-500 transition-colors font-light"
|
||||
|
@@ -9,6 +9,6 @@ export const { GET } = createFromSource(source, (page) => {
|
||||
url: page.url,
|
||||
id: page.url,
|
||||
structuredData: page.data.structuredData,
|
||||
tag: page.url.startsWith("/docs/3.1-beta") ? "v3.1-beta" : "v2.0.0-beta",
|
||||
tag: page.url.startsWith("/docs/3.2-beta") ? "v3.2-beta" : "v2.0.0-beta",
|
||||
};
|
||||
});
|
||||
|
@@ -11,7 +11,7 @@ import { Sponsor } from "../components/sponsor";
|
||||
export default async function Page(props: { params: Promise<{ slug?: string[] }> }) {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) redirect("/docs/3.1-beta");
|
||||
if (!page) redirect("/docs/3.2-beta");
|
||||
|
||||
const MDXContent = page.data.body;
|
||||
|
||||
@@ -49,7 +49,7 @@ export async function generateStaticParams() {
|
||||
export async function generateMetadata(props: { params: Promise<{ slug?: string[] }> }) {
|
||||
const params = await props.params;
|
||||
const page = source.getPage(params.slug);
|
||||
if (!page) redirect("/docs/3.1-beta");
|
||||
if (!page) redirect("/docs/3.2-beta");
|
||||
|
||||
return {
|
||||
title: page.data.title + " | Palmr. Docs",
|
||||
|
@@ -6,7 +6,7 @@ export function Footer() {
|
||||
<div className="flex items-center gap-1 text-sm ">
|
||||
<span>Powered by</span>
|
||||
<Link
|
||||
href="http://kyantech.com.br"
|
||||
href="https://github.com/kyantech"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
className="flex items-center hover:text-green-700 text-green-500 transition-colors font-light"
|
||||
|
@@ -28,7 +28,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
<RootProvider
|
||||
search={{
|
||||
options: {
|
||||
defaultTag: "3.1-beta",
|
||||
defaultTag: "3.2-beta",
|
||||
tags: [
|
||||
{
|
||||
name: "v2.0.0 Beta",
|
||||
@@ -36,7 +36,7 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
},
|
||||
{
|
||||
name: "v3.0 Beta ✨",
|
||||
value: "3.1-beta",
|
||||
value: "3.2-beta",
|
||||
props: {
|
||||
style: {
|
||||
border: "1px solid rgba(0,165,80,0.2)",
|
||||
|
@@ -6,61 +6,61 @@ const providers = [
|
||||
{
|
||||
name: "Google",
|
||||
description: "Configure authentication using Google OAuth2 services",
|
||||
href: "/docs/3.1-beta/oidc-authentication/google",
|
||||
href: "/docs/3.2-beta/oidc-authentication/google",
|
||||
icon: <Chrome className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
name: "Discord",
|
||||
description: "Set up Discord OAuth2 for community-based authentication",
|
||||
href: "/docs/3.1-beta/oidc-authentication/discord",
|
||||
href: "/docs/3.2-beta/oidc-authentication/discord",
|
||||
icon: <MessageSquare className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
name: "GitHub",
|
||||
description: "Enable GitHub OAuth for developer-friendly sign-in",
|
||||
href: "/docs/3.1-beta/oidc-authentication/github",
|
||||
href: "/docs/3.2-beta/oidc-authentication/github",
|
||||
icon: <Github className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
name: "Zitadel",
|
||||
description: "Enterprise-grade identity and access management",
|
||||
href: "/docs/3.1-beta/oidc-authentication/zitadel",
|
||||
href: "/docs/3.2-beta/oidc-authentication/zitadel",
|
||||
icon: <Shield className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
name: "Auth0",
|
||||
description: "Flexible identity platform with extensive customization",
|
||||
href: "/docs/3.1-beta/oidc-authentication/auth0",
|
||||
href: "/docs/3.2-beta/oidc-authentication/auth0",
|
||||
icon: <Lock className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
name: "Authentik",
|
||||
description: "Open-source identity provider with modern features",
|
||||
href: "/docs/3.1-beta/oidc-authentication/authentik",
|
||||
href: "/docs/3.2-beta/oidc-authentication/authentik",
|
||||
icon: <Key className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
name: "Frontegg",
|
||||
description: "User management platform for B2B applications",
|
||||
href: "/docs/3.1-beta/oidc-authentication/frontegg",
|
||||
href: "/docs/3.2-beta/oidc-authentication/frontegg",
|
||||
icon: <Egg className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
name: "Kinde Auth",
|
||||
description: "Developer-first authentication and user management",
|
||||
href: "/docs/3.1-beta/oidc-authentication/kinde-auth",
|
||||
href: "/docs/3.2-beta/oidc-authentication/kinde-auth",
|
||||
icon: <Users className="w-4 h-" />,
|
||||
},
|
||||
{
|
||||
name: "Pocket ID",
|
||||
description: "Open-source identity provider with OIDC support",
|
||||
href: "/docs/3.1-beta/oidc-authentication/pocket-id",
|
||||
href: "/docs/3.2-beta/oidc-authentication/pocket-id",
|
||||
icon: <Key className="w-4 h-4" />,
|
||||
},
|
||||
{
|
||||
name: "Other",
|
||||
description: "Configure any other OIDC-compliant identity provider",
|
||||
href: "/docs/3.1-beta/oidc-authentication/other",
|
||||
href: "/docs/3.2-beta/oidc-authentication/other",
|
||||
icon: <Settings className="w-4 h-4" />,
|
||||
},
|
||||
];
|
||||
|
@@ -1,2 +1,2 @@
|
||||
export const LATEST_VERSION_PATH = "/docs/3.1-beta";
|
||||
export const LATEST_VERSION = "v3.1-beta";
|
||||
export const LATEST_VERSION_PATH = "/docs/3.2-beta";
|
||||
export const LATEST_VERSION = "v3.2-beta";
|
||||
|
@@ -14,3 +14,5 @@ DATABASE_URL="file:./palmr.db"
|
||||
# S3_REGION=
|
||||
# S3_BUCKET_NAME=
|
||||
# S3_FORCE_PATH_STYLE=
|
||||
# S3_REJECT_UNAUTHORIZED=true # Set to false to disable strict SSL certificate validation for self-signed certificates (optional, defaults to true)
|
||||
# PRESIGNED_URL_EXPIRATION=3600 # Duration in seconds for presigned URL expiration (optional, defaults to 3600 seconds / 1 hour)
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-api",
|
||||
"version": "3.1.8-beta",
|
||||
"version": "3.2.3-beta",
|
||||
"description": "API for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
@@ -77,4 +77,4 @@
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,287 +1,319 @@
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
firstName String
|
||||
lastName String
|
||||
username String @unique
|
||||
email String @unique
|
||||
password String?
|
||||
image String?
|
||||
isAdmin Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorSecret String?
|
||||
twoFactorBackupCodes String?
|
||||
twoFactorVerified Boolean @default(false)
|
||||
|
||||
files File[]
|
||||
shares Share[]
|
||||
reverseShares ReverseShare[]
|
||||
|
||||
loginAttempts LoginAttempt?
|
||||
|
||||
passwordResets PasswordReset[]
|
||||
authProviders UserAuthProvider[]
|
||||
trustedDevices TrustedDevice[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model File {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
extension String
|
||||
size BigInt
|
||||
objectName String
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
shares Share[] @relation("ShareFiles")
|
||||
|
||||
@@map("files")
|
||||
}
|
||||
|
||||
model Share {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
views Int @default(0)
|
||||
expiration DateTime?
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
creatorId String?
|
||||
creator User? @relation(fields: [creatorId], references: [id], onDelete: SetNull)
|
||||
|
||||
securityId String @unique
|
||||
security ShareSecurity @relation(fields: [securityId], references: [id])
|
||||
|
||||
files File[] @relation("ShareFiles")
|
||||
recipients ShareRecipient[]
|
||||
|
||||
alias ShareAlias?
|
||||
|
||||
@@map("shares")
|
||||
}
|
||||
|
||||
model ShareSecurity {
|
||||
id String @id @default(cuid())
|
||||
password String?
|
||||
maxViews Int?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
share Share?
|
||||
|
||||
@@map("share_security")
|
||||
}
|
||||
|
||||
model ShareRecipient {
|
||||
id String @id @default(cuid())
|
||||
email String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
shareId String
|
||||
share Share @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("share_recipients")
|
||||
}
|
||||
|
||||
model AppConfig {
|
||||
id String @id @default(cuid())
|
||||
key String @unique
|
||||
value String
|
||||
type String
|
||||
group String
|
||||
isSystem Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("app_configs")
|
||||
}
|
||||
|
||||
model LoginAttempt {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
attempts Int @default(1)
|
||||
lastAttempt DateTime @default(now())
|
||||
|
||||
@@map("login_attempts")
|
||||
}
|
||||
|
||||
model PasswordReset {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
token String @unique
|
||||
expiresAt DateTime
|
||||
used Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("password_resets")
|
||||
}
|
||||
|
||||
model ShareAlias {
|
||||
id String @id @default(cuid())
|
||||
alias String @unique
|
||||
shareId String @unique
|
||||
share Share @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("share_aliases")
|
||||
}
|
||||
|
||||
model AuthProvider {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
displayName String
|
||||
type String
|
||||
icon String?
|
||||
enabled Boolean @default(false)
|
||||
|
||||
issuerUrl String?
|
||||
clientId String?
|
||||
clientSecret String?
|
||||
redirectUri String?
|
||||
scope String? @default("openid profile email")
|
||||
|
||||
authorizationEndpoint String?
|
||||
tokenEndpoint String?
|
||||
userInfoEndpoint String?
|
||||
|
||||
metadata String?
|
||||
|
||||
autoRegister Boolean @default(true)
|
||||
adminEmailDomains String?
|
||||
|
||||
sortOrder Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
userAuthProviders UserAuthProvider[]
|
||||
|
||||
@@map("auth_providers")
|
||||
}
|
||||
|
||||
model UserAuthProvider {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
providerId String
|
||||
authProvider AuthProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
||||
|
||||
provider String?
|
||||
|
||||
externalId String
|
||||
metadata String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([userId, providerId])
|
||||
@@unique([providerId, externalId])
|
||||
@@map("user_auth_providers")
|
||||
}
|
||||
|
||||
model ReverseShare {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
description String?
|
||||
expiration DateTime?
|
||||
maxFiles Int?
|
||||
maxFileSize BigInt?
|
||||
allowedFileTypes String?
|
||||
password String?
|
||||
pageLayout PageLayout @default(DEFAULT)
|
||||
isActive Boolean @default(true)
|
||||
nameFieldRequired FieldRequirement @default(OPTIONAL)
|
||||
emailFieldRequired FieldRequirement @default(OPTIONAL)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
creatorId String
|
||||
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||
|
||||
files ReverseShareFile[]
|
||||
alias ReverseShareAlias?
|
||||
|
||||
@@map("reverse_shares")
|
||||
}
|
||||
|
||||
model ReverseShareFile {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
extension String
|
||||
size BigInt
|
||||
objectName String
|
||||
uploaderEmail String?
|
||||
uploaderName String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
reverseShareId String
|
||||
reverseShare ReverseShare @relation(fields: [reverseShareId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("reverse_share_files")
|
||||
}
|
||||
|
||||
model ReverseShareAlias {
|
||||
id String @id @default(cuid())
|
||||
alias String @unique
|
||||
reverseShareId String @unique
|
||||
reverseShare ReverseShare @relation(fields: [reverseShareId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("reverse_share_aliases")
|
||||
}
|
||||
|
||||
enum FieldRequirement {
|
||||
HIDDEN
|
||||
OPTIONAL
|
||||
REQUIRED
|
||||
}
|
||||
|
||||
enum PageLayout {
|
||||
DEFAULT
|
||||
WETRANSFER
|
||||
}
|
||||
|
||||
model TrustedDevice {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
deviceHash String @unique
|
||||
deviceName String?
|
||||
userAgent String?
|
||||
ipAddress String?
|
||||
lastUsedAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("trusted_devices")
|
||||
}
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
firstName String
|
||||
lastName String
|
||||
username String @unique
|
||||
email String @unique
|
||||
password String?
|
||||
image String?
|
||||
isAdmin Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
twoFactorEnabled Boolean @default(false)
|
||||
twoFactorSecret String?
|
||||
twoFactorBackupCodes String?
|
||||
twoFactorVerified Boolean @default(false)
|
||||
|
||||
files File[]
|
||||
folders Folder[]
|
||||
shares Share[]
|
||||
reverseShares ReverseShare[]
|
||||
|
||||
loginAttempts LoginAttempt?
|
||||
|
||||
passwordResets PasswordReset[]
|
||||
authProviders UserAuthProvider[]
|
||||
trustedDevices TrustedDevice[]
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model File {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
extension String
|
||||
size BigInt
|
||||
objectName String
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
folderId String?
|
||||
folder Folder? @relation(fields: [folderId], references: [id], onDelete: Cascade)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
shares Share[] @relation("ShareFiles")
|
||||
|
||||
@@index([folderId])
|
||||
|
||||
@@map("files")
|
||||
}
|
||||
|
||||
model Share {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
views Int @default(0)
|
||||
expiration DateTime?
|
||||
description String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
creatorId String?
|
||||
creator User? @relation(fields: [creatorId], references: [id], onDelete: SetNull)
|
||||
|
||||
securityId String @unique
|
||||
security ShareSecurity @relation(fields: [securityId], references: [id])
|
||||
|
||||
files File[] @relation("ShareFiles")
|
||||
folders Folder[] @relation("ShareFolders")
|
||||
recipients ShareRecipient[]
|
||||
|
||||
alias ShareAlias?
|
||||
|
||||
@@map("shares")
|
||||
}
|
||||
|
||||
model ShareSecurity {
|
||||
id String @id @default(cuid())
|
||||
password String?
|
||||
maxViews Int?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
share Share?
|
||||
|
||||
@@map("share_security")
|
||||
}
|
||||
|
||||
model ShareRecipient {
|
||||
id String @id @default(cuid())
|
||||
email String
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
shareId String
|
||||
share Share @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("share_recipients")
|
||||
}
|
||||
|
||||
model AppConfig {
|
||||
id String @id @default(cuid())
|
||||
key String @unique
|
||||
value String
|
||||
type String
|
||||
group String
|
||||
isSystem Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("app_configs")
|
||||
}
|
||||
|
||||
model LoginAttempt {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
attempts Int @default(1)
|
||||
lastAttempt DateTime @default(now())
|
||||
|
||||
@@map("login_attempts")
|
||||
}
|
||||
|
||||
model PasswordReset {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
token String @unique
|
||||
expiresAt DateTime
|
||||
used Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("password_resets")
|
||||
}
|
||||
|
||||
model ShareAlias {
|
||||
id String @id @default(cuid())
|
||||
alias String @unique
|
||||
shareId String @unique
|
||||
share Share @relation(fields: [shareId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("share_aliases")
|
||||
}
|
||||
|
||||
model AuthProvider {
|
||||
id String @id @default(cuid())
|
||||
name String @unique
|
||||
displayName String
|
||||
type String
|
||||
icon String?
|
||||
enabled Boolean @default(false)
|
||||
|
||||
issuerUrl String?
|
||||
clientId String?
|
||||
clientSecret String?
|
||||
redirectUri String?
|
||||
scope String? @default("openid profile email")
|
||||
|
||||
authorizationEndpoint String?
|
||||
tokenEndpoint String?
|
||||
userInfoEndpoint String?
|
||||
|
||||
metadata String?
|
||||
|
||||
autoRegister Boolean @default(true)
|
||||
adminEmailDomains String?
|
||||
|
||||
sortOrder Int @default(0)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
userAuthProviders UserAuthProvider[]
|
||||
|
||||
@@map("auth_providers")
|
||||
}
|
||||
|
||||
model UserAuthProvider {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
providerId String
|
||||
authProvider AuthProvider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
||||
|
||||
provider String?
|
||||
|
||||
externalId String
|
||||
metadata String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([userId, providerId])
|
||||
@@unique([providerId, externalId])
|
||||
@@map("user_auth_providers")
|
||||
}
|
||||
|
||||
model ReverseShare {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
description String?
|
||||
expiration DateTime?
|
||||
maxFiles Int?
|
||||
maxFileSize BigInt?
|
||||
allowedFileTypes String?
|
||||
password String?
|
||||
pageLayout PageLayout @default(DEFAULT)
|
||||
isActive Boolean @default(true)
|
||||
nameFieldRequired FieldRequirement @default(OPTIONAL)
|
||||
emailFieldRequired FieldRequirement @default(OPTIONAL)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
creatorId String
|
||||
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||
|
||||
files ReverseShareFile[]
|
||||
alias ReverseShareAlias?
|
||||
|
||||
@@map("reverse_shares")
|
||||
}
|
||||
|
||||
model ReverseShareFile {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
extension String
|
||||
size BigInt
|
||||
objectName String
|
||||
uploaderEmail String?
|
||||
uploaderName String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
reverseShareId String
|
||||
reverseShare ReverseShare @relation(fields: [reverseShareId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("reverse_share_files")
|
||||
}
|
||||
|
||||
model ReverseShareAlias {
|
||||
id String @id @default(cuid())
|
||||
alias String @unique
|
||||
reverseShareId String @unique
|
||||
reverseShare ReverseShare @relation(fields: [reverseShareId], references: [id], onDelete: Cascade)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("reverse_share_aliases")
|
||||
}
|
||||
|
||||
enum FieldRequirement {
|
||||
HIDDEN
|
||||
OPTIONAL
|
||||
REQUIRED
|
||||
}
|
||||
|
||||
enum PageLayout {
|
||||
DEFAULT
|
||||
WETRANSFER
|
||||
}
|
||||
|
||||
model TrustedDevice {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
deviceHash String @unique
|
||||
deviceName String?
|
||||
userAgent String?
|
||||
ipAddress String?
|
||||
lastUsedAt DateTime @default(now())
|
||||
expiresAt DateTime
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@map("trusted_devices")
|
||||
}
|
||||
|
||||
model Folder {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
objectName String
|
||||
|
||||
parentId String?
|
||||
parent Folder? @relation("FolderHierarchy", fields: [parentId], references: [id], onDelete: Cascade)
|
||||
children Folder[] @relation("FolderHierarchy")
|
||||
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
files File[]
|
||||
|
||||
shares Share[] @relation("ShareFolders")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([userId])
|
||||
@@index([parentId])
|
||||
@@map("folders")
|
||||
}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import crypto from "node:crypto";
|
||||
import * as http from "node:http";
|
||||
import fastifyCookie from "@fastify/cookie";
|
||||
import { fastifyCors } from "@fastify/cors";
|
||||
import fastifyJwt from "@fastify/jwt";
|
||||
@@ -31,6 +32,31 @@ export async function buildApp() {
|
||||
keepAliveTimeout: envTimeoutOverrides.keepAliveTimeout,
|
||||
requestTimeout: envTimeoutOverrides.requestTimeout,
|
||||
trustProxy: true,
|
||||
maxParamLength: 500,
|
||||
onProtoPoisoning: "ignore",
|
||||
onConstructorPoisoning: "ignore",
|
||||
ignoreTrailingSlash: true,
|
||||
serverFactory: (handler: (req: any, res: any) => void) => {
|
||||
const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
res.setTimeout(0);
|
||||
req.setTimeout(0);
|
||||
|
||||
req.on("close", () => {
|
||||
if (typeof global !== "undefined" && global.gc) {
|
||||
setImmediate(() => global.gc!());
|
||||
}
|
||||
});
|
||||
|
||||
handler(req, res);
|
||||
});
|
||||
|
||||
server.maxHeadersCount = 0;
|
||||
server.timeout = 0;
|
||||
server.keepAliveTimeout = envTimeoutOverrides.keepAliveTimeout;
|
||||
server.headersTimeout = envTimeoutOverrides.keepAliveTimeout + 1000;
|
||||
|
||||
return server;
|
||||
},
|
||||
}).withTypeProvider<ZodTypeProvider>();
|
||||
|
||||
app.setValidatorCompiler(validatorCompiler);
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import process from "node:process";
|
||||
import { S3Client } from "@aws-sdk/client-s3";
|
||||
|
||||
import { env } from "../env";
|
||||
@@ -14,6 +15,14 @@ export const storageConfig: StorageConfig = {
|
||||
forcePathStyle: env.S3_FORCE_PATH_STYLE === "true",
|
||||
};
|
||||
|
||||
if (storageConfig.useSSL && env.S3_REJECT_UNAUTHORIZED === "false") {
|
||||
const originalRejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||
if (!originalRejectUnauthorized) {
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
||||
(global as any).PALMR_ORIGINAL_TLS_SETTING = originalRejectUnauthorized;
|
||||
}
|
||||
}
|
||||
|
||||
export const s3Client =
|
||||
env.ENABLE_S3 === "true"
|
||||
? new S3Client({
|
||||
|
@@ -18,6 +18,7 @@ export function registerSwagger(app: any) {
|
||||
{ name: "Auth Providers", description: "External authentication providers management" },
|
||||
{ name: "User", description: "User management endpoints" },
|
||||
{ name: "File", description: "File management endpoints" },
|
||||
{ name: "Folder", description: "Folder management endpoints" },
|
||||
{ name: "Share", description: "File sharing endpoints" },
|
||||
{ name: "Storage", description: "Storage management endpoints" },
|
||||
{ name: "App", description: "Application configuration endpoints" },
|
||||
|
@@ -12,8 +12,28 @@ const envSchema = z.object({
|
||||
S3_REGION: z.string().optional(),
|
||||
S3_BUCKET_NAME: z.string().optional(),
|
||||
S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"),
|
||||
S3_REJECT_UNAUTHORIZED: z.union([z.literal("true"), z.literal("false")]).default("true"),
|
||||
PRESIGNED_URL_EXPIRATION: z.string().optional().default("3600"),
|
||||
SECURE_SITE: z.union([z.literal("true"), z.literal("false")]).default("false"),
|
||||
DATABASE_URL: z.string().optional().default("file:/app/server/prisma/palmr.db"),
|
||||
DOWNLOAD_MAX_CONCURRENT: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val ? parseInt(val, 10) : undefined)),
|
||||
DOWNLOAD_MEMORY_THRESHOLD_MB: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val ? parseInt(val, 10) : undefined)),
|
||||
DOWNLOAD_QUEUE_SIZE: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val ? parseInt(val, 10) : undefined)),
|
||||
DOWNLOAD_AUTO_SCALE: z.union([z.literal("true"), z.literal("false")]).default("true"),
|
||||
DOWNLOAD_MIN_FILE_SIZE_GB: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => (val ? parseFloat(val) : undefined)),
|
||||
CUSTOM_PATH: z.string().optional(),
|
||||
});
|
||||
|
||||
export const env = envSchema.parse(process.env);
|
||||
|
@@ -9,7 +9,7 @@ export class AppController {
|
||||
private logoService = new LogoService();
|
||||
private emailService = new EmailService();
|
||||
|
||||
async getAppInfo(request: FastifyRequest, reply: FastifyReply) {
|
||||
async getAppInfo(_request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const appInfo = await this.appService.getAppInfo();
|
||||
return reply.send(appInfo);
|
||||
@@ -18,7 +18,7 @@ export class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
async getSystemInfo(request: FastifyRequest, reply: FastifyReply) {
|
||||
async getSystemInfo(_request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const systemInfo = await this.appService.getSystemInfo();
|
||||
return reply.send(systemInfo);
|
||||
@@ -27,7 +27,7 @@ export class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
async getAllConfigs(request: FastifyRequest, reply: FastifyReply) {
|
||||
async getAllConfigs(_request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const configs = await this.appService.getAllConfigs();
|
||||
return reply.send({ configs });
|
||||
@@ -36,6 +36,15 @@ export class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
async getPublicConfigs(_request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const configs = await this.appService.getPublicConfigs();
|
||||
return reply.send({ configs });
|
||||
} catch (error: any) {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async updateConfig(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { key } = request.params as { key: string };
|
||||
@@ -90,9 +99,8 @@ export class AppController {
|
||||
return reply.status(400).send({ error: "Only images are allowed" });
|
||||
}
|
||||
|
||||
// Logo files should be small (max 5MB), so we can safely use streaming to buffer
|
||||
const chunks: Buffer[] = [];
|
||||
const maxLogoSize = 5 * 1024 * 1024; // 5MB
|
||||
const maxLogoSize = 5 * 1024 * 1024;
|
||||
let totalSize = 0;
|
||||
|
||||
for await (const chunk of file.file) {
|
||||
@@ -114,7 +122,7 @@ export class AppController {
|
||||
}
|
||||
}
|
||||
|
||||
async removeLogo(request: FastifyRequest, reply: FastifyReply) {
|
||||
async removeLogo(_request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await this.logoService.deleteLogo();
|
||||
return reply.send({ message: "Logo removed successfully" });
|
||||
|
@@ -102,15 +102,34 @@ export async function appRoutes(app: FastifyInstance) {
|
||||
appController.updateConfig.bind(appController)
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/app/configs/public",
|
||||
{
|
||||
schema: {
|
||||
tags: ["App"],
|
||||
operationId: "getPublicConfigs",
|
||||
summary: "List public configurations",
|
||||
description: "List public configurations (excludes sensitive data like SMTP credentials)",
|
||||
response: {
|
||||
200: z.object({
|
||||
configs: z.array(ConfigResponseSchema),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
appController.getPublicConfigs.bind(appController)
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/app/configs",
|
||||
{
|
||||
// preValidation: adminPreValidation,
|
||||
preValidation: adminPreValidation,
|
||||
schema: {
|
||||
tags: ["App"],
|
||||
operationId: "getAllConfigs",
|
||||
summary: "List all configurations",
|
||||
description: "List all configurations (admin only)",
|
||||
description: "List all configurations including sensitive data (admin only)",
|
||||
response: {
|
||||
200: z.object({
|
||||
configs: z.array(ConfigResponseSchema),
|
||||
|
@@ -41,6 +41,30 @@ export class AppService {
|
||||
});
|
||||
}
|
||||
|
||||
async getPublicConfigs() {
|
||||
const sensitiveKeys = [
|
||||
"smtpHost",
|
||||
"smtpPort",
|
||||
"smtpUser",
|
||||
"smtpPass",
|
||||
"smtpSecure",
|
||||
"smtpNoAuth",
|
||||
"smtpTrustSelfSigned",
|
||||
"jwtSecret",
|
||||
];
|
||||
|
||||
return prisma.appConfig.findMany({
|
||||
where: {
|
||||
key: {
|
||||
notIn: sensitiveKeys,
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
group: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateConfig(key: string, value: string) {
|
||||
if (key === "jwtSecret") {
|
||||
throw new Error("JWT Secret cannot be updated through this endpoint");
|
||||
|
@@ -1,9 +1,21 @@
|
||||
import bcrypt from "bcryptjs";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { env } from "../../env";
|
||||
import { prisma } from "../../shared/prisma";
|
||||
import { ConfigService } from "../config/service";
|
||||
import { CheckFileInput, CheckFileSchema, RegisterFileInput, RegisterFileSchema, UpdateFileSchema } from "./dto";
|
||||
import {
|
||||
CheckFileInput,
|
||||
CheckFileSchema,
|
||||
ListFilesInput,
|
||||
ListFilesSchema,
|
||||
MoveFileInput,
|
||||
MoveFileSchema,
|
||||
RegisterFileInput,
|
||||
RegisterFileSchema,
|
||||
UpdateFileInput,
|
||||
UpdateFileSchema,
|
||||
} from "./dto";
|
||||
import { FileService } from "./service";
|
||||
|
||||
export class FileController {
|
||||
@@ -28,7 +40,7 @@ export class FileController {
|
||||
}
|
||||
|
||||
const objectName = `${userId}/${Date.now()}-${filename}.${extension}`;
|
||||
const expires = 3600;
|
||||
const expires = parseInt(env.PRESIGNED_URL_EXPIRATION);
|
||||
|
||||
const url = await this.fileService.getPresignedPutUrl(objectName, expires);
|
||||
return reply.send({ url, objectName });
|
||||
@@ -72,6 +84,15 @@ export class FileController {
|
||||
});
|
||||
}
|
||||
|
||||
if (input.folderId) {
|
||||
const folder = await prisma.folder.findFirst({
|
||||
where: { id: input.folderId, userId },
|
||||
});
|
||||
if (!folder) {
|
||||
return reply.status(400).send({ error: "Folder not found or access denied." });
|
||||
}
|
||||
}
|
||||
|
||||
const fileRecord = await prisma.file.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
@@ -80,6 +101,7 @@ export class FileController {
|
||||
size: BigInt(input.size),
|
||||
objectName: input.objectName,
|
||||
userId,
|
||||
folderId: input.folderId,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -91,6 +113,7 @@ export class FileController {
|
||||
size: fileRecord.size.toString(),
|
||||
objectName: fileRecord.objectName,
|
||||
userId: fileRecord.userId,
|
||||
folderId: fileRecord.folderId,
|
||||
createdAt: fileRecord.createdAt,
|
||||
updatedAt: fileRecord.updatedAt,
|
||||
};
|
||||
@@ -161,6 +184,7 @@ export class FileController {
|
||||
objectName: string;
|
||||
};
|
||||
const objectName = decodeURIComponent(encodedObjectName);
|
||||
const { password } = request.query as { password?: string };
|
||||
|
||||
if (!objectName) {
|
||||
return reply.status(400).send({ error: "The 'objectName' parameter is required." });
|
||||
@@ -171,8 +195,53 @@ export class FileController {
|
||||
if (!fileRecord) {
|
||||
return reply.status(404).send({ error: "File not found." });
|
||||
}
|
||||
|
||||
let hasAccess = false;
|
||||
|
||||
console.log("Requested file with password " + password);
|
||||
|
||||
const shares = await prisma.share.findMany({
|
||||
where: {
|
||||
files: {
|
||||
some: {
|
||||
id: fileRecord.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: {
|
||||
security: true,
|
||||
},
|
||||
});
|
||||
|
||||
for (const share of shares) {
|
||||
if (!share.security.password) {
|
||||
hasAccess = true;
|
||||
break;
|
||||
} else if (password) {
|
||||
const isPasswordValid = await bcrypt.compare(password, share.security.password);
|
||||
if (isPasswordValid) {
|
||||
hasAccess = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
if (userId && fileRecord.userId === userId) {
|
||||
hasAccess = true;
|
||||
}
|
||||
} catch (err) {}
|
||||
}
|
||||
|
||||
if (!hasAccess) {
|
||||
return reply.status(401).send({ error: "Unauthorized access to file." });
|
||||
}
|
||||
|
||||
const fileName = fileRecord.name;
|
||||
const expires = 3600;
|
||||
const expires = parseInt(env.PRESIGNED_URL_EXPIRATION);
|
||||
const url = await this.fileService.getPresignedGetUrl(objectName, expires, fileName);
|
||||
return reply.send({ url, expiresIn: expires });
|
||||
} catch (error) {
|
||||
@@ -189,18 +258,43 @@ export class FileController {
|
||||
return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." });
|
||||
}
|
||||
|
||||
const files = await prisma.file.findMany({
|
||||
where: { userId },
|
||||
});
|
||||
const input: ListFilesInput = ListFilesSchema.parse(request.query);
|
||||
const { folderId, recursive: recursiveStr } = input;
|
||||
const recursive = recursiveStr === "false" ? false : true;
|
||||
|
||||
const filesResponse = files.map((file) => ({
|
||||
let files: any[];
|
||||
|
||||
let targetFolderId: string | null;
|
||||
if (folderId === "null" || folderId === "" || !folderId) {
|
||||
targetFolderId = null; // Root folder
|
||||
} else {
|
||||
targetFolderId = folderId;
|
||||
}
|
||||
|
||||
if (recursive) {
|
||||
if (targetFolderId === null) {
|
||||
files = await this.getAllUserFilesRecursively(userId);
|
||||
} else {
|
||||
const { FolderService } = await import("../folder/service.js");
|
||||
const folderService = new FolderService();
|
||||
files = await folderService.getAllFilesInFolder(targetFolderId, userId);
|
||||
}
|
||||
} else {
|
||||
files = await prisma.file.findMany({
|
||||
where: { userId, folderId: targetFolderId },
|
||||
});
|
||||
}
|
||||
|
||||
const filesResponse = files.map((file: any) => ({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
description: file.description,
|
||||
extension: file.extension,
|
||||
size: file.size.toString(),
|
||||
size: typeof file.size === "bigint" ? file.size.toString() : file.size,
|
||||
objectName: file.objectName,
|
||||
userId: file.userId,
|
||||
folderId: file.folderId,
|
||||
relativePath: file.relativePath || null,
|
||||
createdAt: file.createdAt,
|
||||
updatedAt: file.updatedAt,
|
||||
}));
|
||||
@@ -278,6 +372,7 @@ export class FileController {
|
||||
size: updatedFile.size.toString(),
|
||||
objectName: updatedFile.objectName,
|
||||
userId: updatedFile.userId,
|
||||
folderId: updatedFile.folderId,
|
||||
createdAt: updatedFile.createdAt,
|
||||
updatedAt: updatedFile.updatedAt,
|
||||
};
|
||||
@@ -291,4 +386,86 @@ export class FileController {
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async moveFile(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." });
|
||||
}
|
||||
|
||||
const { id } = request.params as { id: string };
|
||||
const input: MoveFileInput = MoveFileSchema.parse(request.body);
|
||||
|
||||
const existingFile = await prisma.file.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
|
||||
if (!existingFile) {
|
||||
return reply.status(404).send({ error: "File not found." });
|
||||
}
|
||||
|
||||
if (input.folderId) {
|
||||
const targetFolder = await prisma.folder.findFirst({
|
||||
where: { id: input.folderId, userId },
|
||||
});
|
||||
if (!targetFolder) {
|
||||
return reply.status(400).send({ error: "Target folder not found." });
|
||||
}
|
||||
}
|
||||
|
||||
const updatedFile = await prisma.file.update({
|
||||
where: { id },
|
||||
data: { folderId: input.folderId },
|
||||
});
|
||||
|
||||
const fileResponse = {
|
||||
id: updatedFile.id,
|
||||
name: updatedFile.name,
|
||||
description: updatedFile.description,
|
||||
extension: updatedFile.extension,
|
||||
size: updatedFile.size.toString(),
|
||||
objectName: updatedFile.objectName,
|
||||
userId: updatedFile.userId,
|
||||
folderId: updatedFile.folderId,
|
||||
createdAt: updatedFile.createdAt,
|
||||
updatedAt: updatedFile.updatedAt,
|
||||
};
|
||||
|
||||
return reply.send({
|
||||
file: fileResponse,
|
||||
message: "File moved successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error moving file:", error);
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
private async getAllUserFilesRecursively(userId: string): Promise<any[]> {
|
||||
const rootFiles = await prisma.file.findMany({
|
||||
where: { userId, folderId: null },
|
||||
});
|
||||
|
||||
const rootFolders = await prisma.folder.findMany({
|
||||
where: { userId, parentId: null },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
let allFiles = [...rootFiles];
|
||||
|
||||
if (rootFolders.length > 0) {
|
||||
const { FolderService } = await import("../folder/service.js");
|
||||
const folderService = new FolderService();
|
||||
|
||||
for (const folder of rootFolders) {
|
||||
const folderFiles = await folderService.getAllFilesInFolder(folder.id, userId);
|
||||
allFiles = [...allFiles, ...folderFiles];
|
||||
}
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ export const RegisterFileSchema = z.object({
|
||||
invalid_type_error: "O tamanho deve ser um número",
|
||||
}),
|
||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||
folderId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const CheckFileSchema = z.object({
|
||||
@@ -20,6 +21,7 @@ export const CheckFileSchema = z.object({
|
||||
invalid_type_error: "O tamanho deve ser um número",
|
||||
}),
|
||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||
folderId: z.string().optional(),
|
||||
});
|
||||
|
||||
export type RegisterFileInput = z.infer<typeof RegisterFileSchema>;
|
||||
@@ -30,4 +32,15 @@ export const UpdateFileSchema = z.object({
|
||||
description: z.string().optional().nullable().describe("The file description"),
|
||||
});
|
||||
|
||||
export const MoveFileSchema = z.object({
|
||||
folderId: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const ListFilesSchema = z.object({
|
||||
folderId: z.string().optional().describe("The folder ID"),
|
||||
recursive: z.string().optional().default("true").describe("Include files from subfolders"),
|
||||
});
|
||||
|
||||
export type UpdateFileInput = z.infer<typeof UpdateFileSchema>;
|
||||
export type MoveFileInput = z.infer<typeof MoveFileSchema>;
|
||||
export type ListFilesInput = z.infer<typeof ListFilesSchema>;
|
||||
|
@@ -2,7 +2,7 @@ import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { FileController } from "./controller";
|
||||
import { CheckFileSchema, RegisterFileSchema, UpdateFileSchema } from "./dto";
|
||||
import { CheckFileSchema, ListFilesSchema, MoveFileSchema, RegisterFileSchema, UpdateFileSchema } from "./dto";
|
||||
|
||||
export async function fileRoutes(app: FastifyInstance) {
|
||||
const fileController = new FileController();
|
||||
@@ -62,6 +62,7 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
size: z.string().describe("The file size"),
|
||||
objectName: z.string().describe("The object name of the file"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
folderId: z.string().nullable().describe("The folder ID"),
|
||||
createdAt: z.date().describe("The file creation date"),
|
||||
updatedAt: z.date().describe("The file last update date"),
|
||||
}),
|
||||
@@ -78,6 +79,7 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
app.post(
|
||||
"/files/check",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["File"],
|
||||
operationId: "checkFile",
|
||||
@@ -110,10 +112,13 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
tags: ["File"],
|
||||
operationId: "getDownloadUrl",
|
||||
summary: "Get Download URL",
|
||||
description: "Generates a pre-signed URL for downloading a private file",
|
||||
description: "Generates a pre-signed URL for downloading a file",
|
||||
params: z.object({
|
||||
objectName: z.string().min(1, "The objectName is required"),
|
||||
}),
|
||||
querystring: z.object({
|
||||
password: z.string().optional().describe("Share password if required"),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
url: z.string().describe("The download URL"),
|
||||
@@ -136,7 +141,8 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
tags: ["File"],
|
||||
operationId: "listFiles",
|
||||
summary: "List Files",
|
||||
description: "Lists user files",
|
||||
description: "Lists user files recursively by default, optionally filtered by folder",
|
||||
querystring: ListFilesSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
files: z.array(
|
||||
@@ -148,6 +154,8 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
size: z.string().describe("The file size"),
|
||||
objectName: z.string().describe("The object name of the file"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
folderId: z.string().nullable().describe("The folder ID"),
|
||||
relativePath: z.string().nullable().describe("The relative path (only for recursive listing)"),
|
||||
createdAt: z.date().describe("The file creation date"),
|
||||
updatedAt: z.date().describe("The file last update date"),
|
||||
})
|
||||
@@ -160,6 +168,84 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
fileController.listFiles.bind(fileController)
|
||||
);
|
||||
|
||||
app.patch(
|
||||
"/files/:id",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["File"],
|
||||
operationId: "updateFile",
|
||||
summary: "Update File Metadata",
|
||||
description: "Updates file metadata in the database",
|
||||
params: z.object({
|
||||
id: z.string().min(1, "The file id is required").describe("The file ID"),
|
||||
}),
|
||||
body: UpdateFileSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
file: z.object({
|
||||
id: z.string().describe("The file ID"),
|
||||
name: z.string().describe("The file name"),
|
||||
description: z.string().nullable().describe("The file description"),
|
||||
extension: z.string().describe("The file extension"),
|
||||
size: z.string().describe("The file size"),
|
||||
objectName: z.string().describe("The object name of the file"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
folderId: z.string().nullable().describe("The folder ID"),
|
||||
createdAt: z.date().describe("The file creation date"),
|
||||
updatedAt: z.date().describe("The file last update date"),
|
||||
}),
|
||||
message: z.string().describe("Success message"),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
401: z.object({ error: z.string().describe("Error message") }),
|
||||
403: z.object({ error: z.string().describe("Error message") }),
|
||||
404: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
fileController.updateFile.bind(fileController)
|
||||
);
|
||||
|
||||
app.put(
|
||||
"/files/:id/move",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["File"],
|
||||
operationId: "moveFile",
|
||||
summary: "Move File",
|
||||
description: "Moves a file to a different folder",
|
||||
params: z.object({
|
||||
id: z.string().min(1, "The file id is required").describe("The file ID"),
|
||||
}),
|
||||
body: MoveFileSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
file: z.object({
|
||||
id: z.string().describe("The file ID"),
|
||||
name: z.string().describe("The file name"),
|
||||
description: z.string().nullable().describe("The file description"),
|
||||
extension: z.string().describe("The file extension"),
|
||||
size: z.string().describe("The file size"),
|
||||
objectName: z.string().describe("The object name of the file"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
folderId: z.string().nullable().describe("The folder ID"),
|
||||
createdAt: z.date().describe("The file creation date"),
|
||||
updatedAt: z.date().describe("The file last update date"),
|
||||
}),
|
||||
message: z.string().describe("Success message"),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
401: z.object({ error: z.string().describe("Error message") }),
|
||||
403: z.object({ error: z.string().describe("Error message") }),
|
||||
404: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
fileController.moveFile.bind(fileController)
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/files/:id",
|
||||
{
|
||||
@@ -185,42 +271,4 @@ export async function fileRoutes(app: FastifyInstance) {
|
||||
},
|
||||
fileController.deleteFile.bind(fileController)
|
||||
);
|
||||
|
||||
app.patch(
|
||||
"/files/:id",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["File"],
|
||||
operationId: "updateFile",
|
||||
summary: "Update File Metadata",
|
||||
description: "Updates file metadata in the database",
|
||||
params: z.object({
|
||||
id: z.string().min(1, "The file id is required").describe("The file ID"),
|
||||
}),
|
||||
body: UpdateFileSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
file: z.object({
|
||||
id: z.string().describe("The file ID"),
|
||||
name: z.string().describe("The file name"),
|
||||
description: z.string().nullable().describe("The file description"),
|
||||
extension: z.string().describe("The file extension"),
|
||||
size: z.string().describe("The file size"),
|
||||
objectName: z.string().describe("The object name of the file"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
createdAt: z.date().describe("The file creation date"),
|
||||
updatedAt: z.date().describe("The file last update date"),
|
||||
}),
|
||||
message: z.string().describe("Success message"),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
401: z.object({ error: z.string().describe("Error message") }),
|
||||
403: z.object({ error: z.string().describe("Error message") }),
|
||||
404: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
fileController.updateFile.bind(fileController)
|
||||
);
|
||||
}
|
||||
|
@@ -3,10 +3,14 @@ import { pipeline } from "stream/promises";
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { FilesystemStorageProvider } from "../../providers/filesystem-storage.provider";
|
||||
import { DownloadCancelResponse, QueueClearResponse, QueueStatusResponse } from "../../types/download-queue";
|
||||
import { DownloadMemoryManager } from "../../utils/download-memory-manager";
|
||||
import { getContentType } from "../../utils/mime-types";
|
||||
import { ChunkManager, ChunkMetadata } from "./chunk-manager";
|
||||
|
||||
export class FilesystemController {
|
||||
private chunkManager = ChunkManager.getInstance();
|
||||
private memoryManager = DownloadMemoryManager.getInstance();
|
||||
|
||||
private encodeFilenameForHeader(filename: string): string {
|
||||
if (!filename || filename.trim() === "") {
|
||||
@@ -165,9 +169,10 @@ export class FilesystemController {
|
||||
}
|
||||
|
||||
async download(request: FastifyRequest, reply: FastifyReply) {
|
||||
const downloadId = `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
try {
|
||||
const { token } = request.params as { token: string };
|
||||
|
||||
const provider = FilesystemStorageProvider.getInstance();
|
||||
|
||||
const tokenData = provider.validateDownloadToken(token);
|
||||
@@ -179,44 +184,87 @@ export class FilesystemController {
|
||||
const filePath = provider.getFilePath(tokenData.objectName);
|
||||
const stats = await fs.promises.stat(filePath);
|
||||
const fileSize = stats.size;
|
||||
|
||||
const fileName = tokenData.fileName || "download";
|
||||
|
||||
const fileSizeMB = fileSize / (1024 * 1024);
|
||||
console.log(`[DOWNLOAD] Requesting slot for ${downloadId}: ${tokenData.objectName} (${fileSizeMB.toFixed(1)}MB)`);
|
||||
|
||||
try {
|
||||
await this.memoryManager.requestDownloadSlot(downloadId, {
|
||||
fileName,
|
||||
fileSize,
|
||||
objectName: tokenData.objectName,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.warn(`[DOWNLOAD] Queue full for ${downloadId}: ${error.message}`);
|
||||
return reply.status(503).send({
|
||||
error: "Download queue is full",
|
||||
message: error.message,
|
||||
retryAfter: 60,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[DOWNLOAD] Starting ${downloadId}: ${tokenData.objectName} (${fileSizeMB.toFixed(1)}MB)`);
|
||||
this.memoryManager.startDownload(downloadId);
|
||||
|
||||
const range = request.headers.range;
|
||||
|
||||
reply.header("Content-Disposition", this.encodeFilenameForHeader(fileName));
|
||||
reply.header("Content-Type", "application/octet-stream");
|
||||
reply.header("Content-Type", getContentType(fileName));
|
||||
reply.header("Accept-Ranges", "bytes");
|
||||
reply.header("X-Download-ID", downloadId);
|
||||
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, "").split("-");
|
||||
const start = parseInt(parts[0], 10);
|
||||
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
||||
reply.raw.on("close", () => {
|
||||
this.memoryManager.endDownload(downloadId);
|
||||
console.log(`[DOWNLOAD] Client disconnected: ${downloadId}`);
|
||||
});
|
||||
|
||||
reply.status(206);
|
||||
reply.header("Content-Range", `bytes ${start}-${end}/${fileSize}`);
|
||||
reply.header("Content-Length", end - start + 1);
|
||||
reply.raw.on("error", () => {
|
||||
this.memoryManager.endDownload(downloadId);
|
||||
console.log(`[DOWNLOAD] Client error: ${downloadId}`);
|
||||
});
|
||||
|
||||
await this.downloadFileRange(reply, provider, tokenData.objectName, start, end);
|
||||
} else {
|
||||
reply.header("Content-Length", fileSize);
|
||||
await this.downloadFileStream(reply, provider, tokenData.objectName);
|
||||
try {
|
||||
if (range) {
|
||||
const parts = range.replace(/bytes=/, "").split("-");
|
||||
const start = parseInt(parts[0], 10);
|
||||
const end = parts[1] ? parseInt(parts[1], 10) : fileSize - 1;
|
||||
|
||||
reply.status(206);
|
||||
reply.header("Content-Range", `bytes ${start}-${end}/${fileSize}`);
|
||||
reply.header("Content-Length", end - start + 1);
|
||||
|
||||
await this.downloadFileRange(reply, provider, tokenData.objectName, start, end, downloadId);
|
||||
} else {
|
||||
reply.header("Content-Length", fileSize);
|
||||
await this.downloadFileStream(reply, provider, tokenData.objectName, downloadId);
|
||||
}
|
||||
|
||||
provider.consumeDownloadToken(token);
|
||||
} finally {
|
||||
this.memoryManager.endDownload(downloadId);
|
||||
}
|
||||
|
||||
provider.consumeDownloadToken(token);
|
||||
} catch (error) {
|
||||
this.memoryManager.endDownload(downloadId);
|
||||
console.error(`[DOWNLOAD] Error in ${downloadId}:`, error);
|
||||
return reply.status(500).send({ error: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadFileStream(reply: FastifyReply, provider: FilesystemStorageProvider, objectName: string) {
|
||||
private async downloadFileStream(
|
||||
reply: FastifyReply,
|
||||
provider: FilesystemStorageProvider,
|
||||
objectName: string,
|
||||
downloadId?: string
|
||||
) {
|
||||
try {
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download start: ${objectName}`);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download start: ${objectName} (${downloadId})`);
|
||||
|
||||
const downloadStream = provider.createDownloadStream(objectName);
|
||||
|
||||
downloadStream.on("error", (error) => {
|
||||
console.error("Download stream error:", error);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download error: ${objectName}`);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download error: ${objectName} (${downloadId})`);
|
||||
if (!reply.sent) {
|
||||
reply.status(500).send({ error: "Download failed" });
|
||||
}
|
||||
@@ -226,15 +274,40 @@ export class FilesystemController {
|
||||
if (downloadStream.readable && typeof (downloadStream as any).destroy === "function") {
|
||||
(downloadStream as any).destroy();
|
||||
}
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download client disconnect: ${objectName}`);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download client disconnect: ${objectName} (${downloadId})`);
|
||||
});
|
||||
|
||||
await pipeline(downloadStream, reply.raw);
|
||||
if (this.memoryManager.shouldThrottleStream()) {
|
||||
console.log(
|
||||
`[MEMORY THROTTLE] ${objectName} - Pausing stream due to high memory usage: ${this.memoryManager.getCurrentMemoryUsageMB().toFixed(0)}MB`
|
||||
);
|
||||
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download complete: ${objectName}`);
|
||||
const { Transform } = require("stream");
|
||||
const memoryManager = this.memoryManager;
|
||||
const throttleStream = new Transform({
|
||||
highWaterMark: 256 * 1024,
|
||||
transform(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null, data?: any) => void) {
|
||||
if (memoryManager.shouldThrottleStream()) {
|
||||
setImmediate(() => {
|
||||
this.push(chunk);
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
this.push(chunk);
|
||||
callback();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
await pipeline(downloadStream, throttleStream, reply.raw);
|
||||
} else {
|
||||
await pipeline(downloadStream, reply.raw);
|
||||
}
|
||||
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download complete: ${objectName} (${downloadId})`);
|
||||
} catch (error) {
|
||||
console.error("Download error:", error);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download failed: ${objectName}`);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download failed: ${objectName} (${downloadId})`);
|
||||
if (!reply.sent) {
|
||||
reply.status(500).send({ error: "Download failed" });
|
||||
}
|
||||
@@ -246,16 +319,19 @@ export class FilesystemController {
|
||||
provider: FilesystemStorageProvider,
|
||||
objectName: string,
|
||||
start: number,
|
||||
end: number
|
||||
end: number,
|
||||
downloadId?: string
|
||||
) {
|
||||
try {
|
||||
FilesystemStorageProvider.logMemoryUsage(`Range download start: ${objectName} (${start}-${end})`);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Range download start: ${objectName} (${start}-${end}) (${downloadId})`);
|
||||
|
||||
const rangeStream = await provider.createDownloadRangeStream(objectName, start, end);
|
||||
|
||||
rangeStream.on("error", (error) => {
|
||||
console.error("Range download stream error:", error);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Range download error: ${objectName} (${start}-${end})`);
|
||||
FilesystemStorageProvider.logMemoryUsage(
|
||||
`Range download error: ${objectName} (${start}-${end}) (${downloadId})`
|
||||
);
|
||||
if (!reply.sent) {
|
||||
reply.status(500).send({ error: "Download failed" });
|
||||
}
|
||||
@@ -265,18 +341,76 @@ export class FilesystemController {
|
||||
if (rangeStream.readable && typeof (rangeStream as any).destroy === "function") {
|
||||
(rangeStream as any).destroy();
|
||||
}
|
||||
FilesystemStorageProvider.logMemoryUsage(`Range download client disconnect: ${objectName} (${start}-${end})`);
|
||||
FilesystemStorageProvider.logMemoryUsage(
|
||||
`Range download client disconnect: ${objectName} (${start}-${end}) (${downloadId})`
|
||||
);
|
||||
});
|
||||
|
||||
await pipeline(rangeStream, reply.raw);
|
||||
|
||||
FilesystemStorageProvider.logMemoryUsage(`Range download complete: ${objectName} (${start}-${end})`);
|
||||
FilesystemStorageProvider.logMemoryUsage(
|
||||
`Range download complete: ${objectName} (${start}-${end}) (${downloadId})`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Range download error:", error);
|
||||
FilesystemStorageProvider.logMemoryUsage(`Range download failed: ${objectName} (${start}-${end})`);
|
||||
FilesystemStorageProvider.logMemoryUsage(
|
||||
`Range download failed: ${objectName} (${start}-${end}) (${downloadId})`
|
||||
);
|
||||
if (!reply.sent) {
|
||||
reply.status(500).send({ error: "Download failed" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getQueueStatus(_request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const queueStatus = this.memoryManager.getQueueStatus();
|
||||
const response: QueueStatusResponse = {
|
||||
status: "success",
|
||||
data: queueStatus,
|
||||
};
|
||||
reply.status(200).send(response);
|
||||
} catch (error) {
|
||||
console.error("Error getting queue status:", error);
|
||||
return reply.status(500).send({ error: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
async cancelQueuedDownload(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const { downloadId } = request.params as { downloadId: string };
|
||||
|
||||
const cancelled = this.memoryManager.cancelQueuedDownload(downloadId);
|
||||
|
||||
if (cancelled) {
|
||||
const response: DownloadCancelResponse = {
|
||||
message: "Download cancelled successfully",
|
||||
downloadId,
|
||||
};
|
||||
reply.status(200).send(response);
|
||||
} else {
|
||||
reply.status(404).send({
|
||||
error: "Download not found in queue",
|
||||
downloadId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cancelling queued download:", error);
|
||||
return reply.status(500).send({ error: "Internal server error" });
|
||||
}
|
||||
}
|
||||
|
||||
async clearDownloadQueue(_request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
const clearedCount = this.memoryManager.clearQueue();
|
||||
const response: QueueClearResponse = {
|
||||
message: "Download queue cleared successfully",
|
||||
clearedCount,
|
||||
};
|
||||
reply.status(200).send(response);
|
||||
} catch (error) {
|
||||
console.error("Error clearing download queue:", error);
|
||||
return reply.status(500).send({ error: "Internal server error" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
95
apps/server/src/modules/filesystem/download-queue-routes.ts
Normal file
95
apps/server/src/modules/filesystem/download-queue-routes.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { FilesystemController } from "./controller";
|
||||
|
||||
export async function downloadQueueRoutes(app: FastifyInstance) {
|
||||
const filesystemController = new FilesystemController();
|
||||
|
||||
app.get(
|
||||
"/filesystem/download-queue/status",
|
||||
{
|
||||
schema: {
|
||||
tags: ["Download Queue"],
|
||||
operationId: "getDownloadQueueStatus",
|
||||
summary: "Get download queue status",
|
||||
description: "Get current status of the download queue including active downloads and queue length",
|
||||
response: {
|
||||
200: z.object({
|
||||
status: z.string(),
|
||||
data: z.object({
|
||||
queueLength: z.number(),
|
||||
maxQueueSize: z.number(),
|
||||
activeDownloads: z.number(),
|
||||
maxConcurrent: z.number(),
|
||||
queuedDownloads: z.array(
|
||||
z.object({
|
||||
downloadId: z.string(),
|
||||
position: z.number(),
|
||||
waitTime: z.number(),
|
||||
fileName: z.string().optional(),
|
||||
fileSize: z.number().optional(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
}),
|
||||
500: z.object({
|
||||
error: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
filesystemController.getQueueStatus.bind(filesystemController)
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/filesystem/download-queue/:downloadId",
|
||||
{
|
||||
schema: {
|
||||
tags: ["Download Queue"],
|
||||
operationId: "cancelQueuedDownload",
|
||||
summary: "Cancel a queued download",
|
||||
description: "Cancel a specific download that is waiting in the queue",
|
||||
params: z.object({
|
||||
downloadId: z.string().describe("Download ID"),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
downloadId: z.string(),
|
||||
}),
|
||||
404: z.object({
|
||||
error: z.string(),
|
||||
downloadId: z.string(),
|
||||
}),
|
||||
500: z.object({
|
||||
error: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
filesystemController.cancelQueuedDownload.bind(filesystemController)
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/filesystem/download-queue",
|
||||
{
|
||||
schema: {
|
||||
tags: ["Download Queue"],
|
||||
operationId: "clearDownloadQueue",
|
||||
summary: "Clear entire download queue",
|
||||
description: "Cancel all downloads waiting in the queue (admin operation)",
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string(),
|
||||
clearedCount: z.number(),
|
||||
}),
|
||||
500: z.object({
|
||||
error: z.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
filesystemController.clearDownloadQueue.bind(filesystemController)
|
||||
);
|
||||
}
|
412
apps/server/src/modules/folder/controller.ts
Normal file
412
apps/server/src/modules/folder/controller.ts
Normal file
@@ -0,0 +1,412 @@
|
||||
import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import { env } from "../../env";
|
||||
import { prisma } from "../../shared/prisma";
|
||||
import { ConfigService } from "../config/service";
|
||||
import {
|
||||
CheckFolderSchema,
|
||||
ListFoldersSchema,
|
||||
MoveFolderSchema,
|
||||
RegisterFolderSchema,
|
||||
UpdateFolderSchema,
|
||||
} from "./dto";
|
||||
import { FolderService } from "./service";
|
||||
|
||||
export class FolderController {
|
||||
private folderService = new FolderService();
|
||||
private configService = new ConfigService();
|
||||
|
||||
async registerFolder(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
if (!userId) {
|
||||
return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." });
|
||||
}
|
||||
|
||||
const input = RegisterFolderSchema.parse(request.body);
|
||||
|
||||
if (input.parentId) {
|
||||
const parentFolder = await prisma.folder.findFirst({
|
||||
where: { id: input.parentId, userId },
|
||||
});
|
||||
if (!parentFolder) {
|
||||
return reply.status(400).send({ error: "Parent folder not found or access denied" });
|
||||
}
|
||||
}
|
||||
|
||||
const existingFolder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
name: input.name,
|
||||
parentId: input.parentId || null,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingFolder) {
|
||||
return reply.status(400).send({ error: "A folder with this name already exists in this location" });
|
||||
}
|
||||
|
||||
const folderRecord = await prisma.folder.create({
|
||||
data: {
|
||||
name: input.name,
|
||||
description: input.description,
|
||||
objectName: input.objectName,
|
||||
parentId: input.parentId,
|
||||
userId,
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
files: true,
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const totalSize = await this.folderService.calculateFolderSize(folderRecord.id, userId);
|
||||
|
||||
const folderResponse = {
|
||||
id: folderRecord.id,
|
||||
name: folderRecord.name,
|
||||
description: folderRecord.description,
|
||||
objectName: folderRecord.objectName,
|
||||
parentId: folderRecord.parentId,
|
||||
userId: folderRecord.userId,
|
||||
createdAt: folderRecord.createdAt,
|
||||
updatedAt: folderRecord.updatedAt,
|
||||
totalSize: totalSize.toString(),
|
||||
_count: folderRecord._count,
|
||||
};
|
||||
|
||||
return reply.status(201).send({
|
||||
folder: folderResponse,
|
||||
message: "Folder registered successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error in registerFolder:", error);
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async checkFolder(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
if (!userId) {
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized: a valid token is required to access this resource.",
|
||||
code: "unauthorized",
|
||||
});
|
||||
}
|
||||
|
||||
const input = CheckFolderSchema.parse(request.body);
|
||||
|
||||
if (input.name.length > 100) {
|
||||
return reply.status(400).send({
|
||||
code: "folderNameTooLong",
|
||||
error: "Folder name exceeds maximum length of 100 characters",
|
||||
details: "100",
|
||||
});
|
||||
}
|
||||
|
||||
const existingFolder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
name: input.name,
|
||||
parentId: input.parentId || null,
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingFolder) {
|
||||
return reply.status(400).send({
|
||||
error: "A folder with this name already exists in this location",
|
||||
code: "duplicateFolderName",
|
||||
});
|
||||
}
|
||||
|
||||
return reply.status(201).send({
|
||||
message: "Folder checks succeeded.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error in checkFolder:", error);
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async listFolders(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
if (!userId) {
|
||||
return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." });
|
||||
}
|
||||
|
||||
const input = ListFoldersSchema.parse(request.query);
|
||||
const { parentId, recursive: recursiveStr } = input;
|
||||
const recursive = recursiveStr === "false" ? false : true;
|
||||
|
||||
let folders: any[];
|
||||
|
||||
if (recursive) {
|
||||
folders = await prisma.folder.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
files: true,
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ name: "asc" }],
|
||||
});
|
||||
} else {
|
||||
// Get only direct children of specified parent
|
||||
const targetParentId = parentId === "null" || parentId === "" || !parentId ? null : parentId;
|
||||
folders = await prisma.folder.findMany({
|
||||
where: {
|
||||
userId,
|
||||
parentId: targetParentId,
|
||||
},
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
files: true,
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ name: "asc" }],
|
||||
});
|
||||
}
|
||||
|
||||
const foldersResponse = await Promise.all(
|
||||
folders.map(async (folder) => {
|
||||
const totalSize = await this.folderService.calculateFolderSize(folder.id, userId);
|
||||
return {
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
description: folder.description,
|
||||
objectName: folder.objectName,
|
||||
parentId: folder.parentId,
|
||||
userId: folder.userId,
|
||||
createdAt: folder.createdAt,
|
||||
updatedAt: folder.updatedAt,
|
||||
totalSize: totalSize.toString(),
|
||||
_count: folder._count,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return reply.send({ folders: foldersResponse });
|
||||
} catch (error: any) {
|
||||
console.error("Error in listFolders:", error);
|
||||
return reply.status(500).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async updateFolder(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const { id } = request.params as { id: string };
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({
|
||||
error: "Unauthorized: a valid token is required to access this resource.",
|
||||
});
|
||||
}
|
||||
|
||||
const updateData = UpdateFolderSchema.parse(request.body);
|
||||
|
||||
const folderRecord = await prisma.folder.findUnique({ where: { id } });
|
||||
|
||||
if (!folderRecord) {
|
||||
return reply.status(404).send({ error: "Folder not found." });
|
||||
}
|
||||
|
||||
if (folderRecord.userId !== userId) {
|
||||
return reply.status(403).send({ error: "Access denied." });
|
||||
}
|
||||
|
||||
if (updateData.name && updateData.name !== folderRecord.name) {
|
||||
const duplicateFolder = await prisma.folder.findFirst({
|
||||
where: {
|
||||
name: updateData.name,
|
||||
parentId: folderRecord.parentId,
|
||||
userId,
|
||||
id: { not: id },
|
||||
},
|
||||
});
|
||||
|
||||
if (duplicateFolder) {
|
||||
return reply.status(400).send({ error: "A folder with this name already exists in this location" });
|
||||
}
|
||||
}
|
||||
|
||||
const updatedFolder = await prisma.folder.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
files: true,
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const totalSize = await this.folderService.calculateFolderSize(updatedFolder.id, userId);
|
||||
|
||||
const folderResponse = {
|
||||
id: updatedFolder.id,
|
||||
name: updatedFolder.name,
|
||||
description: updatedFolder.description,
|
||||
objectName: updatedFolder.objectName,
|
||||
parentId: updatedFolder.parentId,
|
||||
userId: updatedFolder.userId,
|
||||
createdAt: updatedFolder.createdAt,
|
||||
updatedAt: updatedFolder.updatedAt,
|
||||
totalSize: totalSize.toString(),
|
||||
_count: updatedFolder._count,
|
||||
};
|
||||
|
||||
return reply.send({
|
||||
folder: folderResponse,
|
||||
message: "Folder updated successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error in updateFolder:", error);
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async moveFolder(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
|
||||
if (!userId) {
|
||||
return reply.status(401).send({ error: "Unauthorized: a valid token is required to access this resource." });
|
||||
}
|
||||
|
||||
const { id } = request.params as { id: string };
|
||||
const body = request.body as any;
|
||||
|
||||
const input = {
|
||||
parentId: body.parentId === undefined ? null : body.parentId,
|
||||
};
|
||||
|
||||
const validatedInput = MoveFolderSchema.parse(input);
|
||||
|
||||
const existingFolder = await prisma.folder.findFirst({
|
||||
where: { id, userId },
|
||||
});
|
||||
|
||||
if (!existingFolder) {
|
||||
return reply.status(404).send({ error: "Folder not found." });
|
||||
}
|
||||
|
||||
if (validatedInput.parentId) {
|
||||
const parentFolder = await prisma.folder.findFirst({
|
||||
where: { id: validatedInput.parentId, userId },
|
||||
});
|
||||
if (!parentFolder) {
|
||||
return reply.status(400).send({ error: "Parent folder not found or access denied" });
|
||||
}
|
||||
|
||||
if (await this.isDescendantOf(validatedInput.parentId, id, userId)) {
|
||||
return reply.status(400).send({ error: "Cannot move a folder into itself or its subfolders" });
|
||||
}
|
||||
}
|
||||
|
||||
const updatedFolder = await prisma.folder.update({
|
||||
where: { id },
|
||||
data: { parentId: validatedInput.parentId },
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
files: true,
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const totalSize = await this.folderService.calculateFolderSize(updatedFolder.id, userId);
|
||||
|
||||
const folderResponse = {
|
||||
id: updatedFolder.id,
|
||||
name: updatedFolder.name,
|
||||
description: updatedFolder.description,
|
||||
objectName: updatedFolder.objectName,
|
||||
parentId: updatedFolder.parentId,
|
||||
userId: updatedFolder.userId,
|
||||
createdAt: updatedFolder.createdAt,
|
||||
updatedAt: updatedFolder.updatedAt,
|
||||
totalSize: totalSize.toString(),
|
||||
_count: updatedFolder._count,
|
||||
};
|
||||
|
||||
return reply.send({
|
||||
folder: folderResponse,
|
||||
message: "Folder moved successfully.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error("Error in moveFolder:", error);
|
||||
const statusCode = error.message === "Folder not found" ? 404 : 400;
|
||||
return reply.status(statusCode).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async deleteFolder(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const { id } = request.params as { id: string };
|
||||
if (!id) {
|
||||
return reply.status(400).send({ error: "The 'id' parameter is required." });
|
||||
}
|
||||
|
||||
const folderRecord = await prisma.folder.findUnique({ where: { id } });
|
||||
if (!folderRecord) {
|
||||
return reply.status(404).send({ error: "Folder not found." });
|
||||
}
|
||||
|
||||
const userId = (request as any).user?.userId;
|
||||
if (folderRecord.userId !== userId) {
|
||||
return reply.status(403).send({ error: "Access denied." });
|
||||
}
|
||||
|
||||
await this.folderService.deleteObject(folderRecord.objectName);
|
||||
|
||||
await prisma.folder.delete({ where: { id } });
|
||||
|
||||
return reply.send({ message: "Folder deleted successfully." });
|
||||
} catch (error) {
|
||||
console.error("Error in deleteFolder:", error);
|
||||
return reply.status(500).send({ error: "Internal server error." });
|
||||
}
|
||||
}
|
||||
|
||||
private async isDescendantOf(potentialDescendantId: string, ancestorId: string, userId: string): Promise<boolean> {
|
||||
let currentId: string | null = potentialDescendantId;
|
||||
|
||||
while (currentId) {
|
||||
if (currentId === ancestorId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const folder: { parentId: string | null } | null = await prisma.folder.findFirst({
|
||||
where: { id: currentId, userId },
|
||||
});
|
||||
|
||||
if (!folder) break;
|
||||
currentId = folder.parentId;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
56
apps/server/src/modules/folder/dto.ts
Normal file
56
apps/server/src/modules/folder/dto.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const RegisterFolderSchema = z.object({
|
||||
name: z.string().min(1, "O nome da pasta é obrigatório"),
|
||||
description: z.string().optional(),
|
||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||
parentId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const UpdateFolderSchema = z.object({
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export const MoveFolderSchema = z.object({
|
||||
parentId: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const FolderResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
parentId: z.string().nullable(),
|
||||
userId: z.string(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date(),
|
||||
totalSize: z
|
||||
.bigint()
|
||||
.transform((val) => val.toString())
|
||||
.optional(),
|
||||
_count: z
|
||||
.object({
|
||||
files: z.number(),
|
||||
children: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const CheckFolderSchema = z.object({
|
||||
name: z.string().min(1, "O nome da pasta é obrigatório"),
|
||||
description: z.string().optional(),
|
||||
objectName: z.string().min(1, "O objectName é obrigatório"),
|
||||
parentId: z.string().optional(),
|
||||
});
|
||||
|
||||
export const ListFoldersSchema = z.object({
|
||||
parentId: z.string().optional(),
|
||||
recursive: z.string().optional().default("true"),
|
||||
});
|
||||
|
||||
export type RegisterFolderInput = z.infer<typeof RegisterFolderSchema>;
|
||||
export type UpdateFolderInput = z.infer<typeof UpdateFolderSchema>;
|
||||
export type MoveFolderInput = z.infer<typeof MoveFolderSchema>;
|
||||
export type CheckFolderInput = z.infer<typeof CheckFolderSchema>;
|
||||
export type ListFoldersInput = z.infer<typeof ListFoldersSchema>;
|
||||
export type FolderResponse = z.infer<typeof FolderResponseSchema>;
|
245
apps/server/src/modules/folder/routes.ts
Normal file
245
apps/server/src/modules/folder/routes.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { z } from "zod";
|
||||
|
||||
import { FolderController } from "./controller";
|
||||
import {
|
||||
CheckFolderSchema,
|
||||
FolderResponseSchema,
|
||||
ListFoldersSchema,
|
||||
MoveFolderSchema,
|
||||
RegisterFolderSchema,
|
||||
UpdateFolderSchema,
|
||||
} from "./dto";
|
||||
|
||||
export async function folderRoutes(app: FastifyInstance) {
|
||||
const folderController = new FolderController();
|
||||
|
||||
const preValidation = async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
reply.status(401).send({ error: "Token inválido ou ausente." });
|
||||
}
|
||||
};
|
||||
|
||||
app.post(
|
||||
"/folders",
|
||||
{
|
||||
schema: {
|
||||
tags: ["Folder"],
|
||||
operationId: "registerFolder",
|
||||
summary: "Register Folder Metadata",
|
||||
description: "Registers folder metadata in the database",
|
||||
body: RegisterFolderSchema,
|
||||
response: {
|
||||
201: z.object({
|
||||
folder: z.object({
|
||||
id: z.string().describe("The folder ID"),
|
||||
name: z.string().describe("The folder name"),
|
||||
description: z.string().nullable().describe("The folder description"),
|
||||
parentId: z.string().nullable().describe("The parent folder ID"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
createdAt: z.date().describe("The folder creation date"),
|
||||
updatedAt: z.date().describe("The folder last update date"),
|
||||
totalSize: z.string().optional().describe("The total size of the folder"),
|
||||
_count: z
|
||||
.object({
|
||||
files: z.number().describe("Number of files in folder"),
|
||||
children: z.number().describe("Number of subfolders"),
|
||||
})
|
||||
.optional()
|
||||
.describe("Count statistics"),
|
||||
}),
|
||||
message: z.string().describe("The folder registration message"),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
401: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
folderController.registerFolder.bind(folderController)
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/folders/check",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["Folder"],
|
||||
operationId: "checkFolder",
|
||||
summary: "Check Folder validity",
|
||||
description: "Checks if the folder meets all requirements",
|
||||
body: CheckFolderSchema,
|
||||
response: {
|
||||
201: z.object({
|
||||
message: z.string().describe("The folder check success message"),
|
||||
}),
|
||||
400: z.object({
|
||||
error: z.string().describe("Error message"),
|
||||
code: z.string().optional().describe("Error code"),
|
||||
details: z.string().optional().describe("Error details"),
|
||||
}),
|
||||
401: z.object({
|
||||
error: z.string().describe("Error message"),
|
||||
code: z.string().optional().describe("Error code"),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
folderController.checkFolder.bind(folderController)
|
||||
);
|
||||
|
||||
app.get(
|
||||
"/folders",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["Folder"],
|
||||
operationId: "listFolders",
|
||||
summary: "List Folders",
|
||||
description: "Lists user folders recursively by default, optionally filtered by folder",
|
||||
querystring: ListFoldersSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
folders: z.array(
|
||||
z.object({
|
||||
id: z.string().describe("The folder ID"),
|
||||
name: z.string().describe("The folder name"),
|
||||
description: z.string().nullable().describe("The folder description"),
|
||||
parentId: z.string().nullable().describe("The parent folder ID"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
createdAt: z.date().describe("The folder creation date"),
|
||||
updatedAt: z.date().describe("The folder last update date"),
|
||||
totalSize: z.string().optional().describe("The total size of the folder"),
|
||||
_count: z
|
||||
.object({
|
||||
files: z.number().describe("Number of files in folder"),
|
||||
children: z.number().describe("Number of subfolders"),
|
||||
})
|
||||
.optional()
|
||||
.describe("Count statistics"),
|
||||
})
|
||||
),
|
||||
}),
|
||||
500: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
folderController.listFolders.bind(folderController)
|
||||
);
|
||||
|
||||
app.patch(
|
||||
"/folders/:id",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["Folder"],
|
||||
operationId: "updateFolder",
|
||||
summary: "Update Folder Metadata",
|
||||
description: "Updates folder metadata in the database",
|
||||
params: z.object({
|
||||
id: z.string().min(1, "The folder id is required").describe("The folder ID"),
|
||||
}),
|
||||
body: UpdateFolderSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
folder: z.object({
|
||||
id: z.string().describe("The folder ID"),
|
||||
name: z.string().describe("The folder name"),
|
||||
description: z.string().nullable().describe("The folder description"),
|
||||
parentId: z.string().nullable().describe("The parent folder ID"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
createdAt: z.date().describe("The folder creation date"),
|
||||
updatedAt: z.date().describe("The folder last update date"),
|
||||
totalSize: z.string().optional().describe("The total size of the folder"),
|
||||
_count: z
|
||||
.object({
|
||||
files: z.number().describe("Number of files in folder"),
|
||||
children: z.number().describe("Number of subfolders"),
|
||||
})
|
||||
.optional()
|
||||
.describe("Count statistics"),
|
||||
}),
|
||||
message: z.string().describe("Success message"),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
401: z.object({ error: z.string().describe("Error message") }),
|
||||
403: z.object({ error: z.string().describe("Error message") }),
|
||||
404: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
folderController.updateFolder.bind(folderController)
|
||||
);
|
||||
|
||||
app.put(
|
||||
"/folders/:id/move",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["Folder"],
|
||||
operationId: "moveFolder",
|
||||
summary: "Move Folder",
|
||||
description: "Moves a folder to a different parent folder",
|
||||
params: z.object({
|
||||
id: z.string().min(1, "The folder id is required").describe("The folder ID"),
|
||||
}),
|
||||
body: MoveFolderSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
folder: z.object({
|
||||
id: z.string().describe("The folder ID"),
|
||||
name: z.string().describe("The folder name"),
|
||||
description: z.string().nullable().describe("The folder description"),
|
||||
parentId: z.string().nullable().describe("The parent folder ID"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
createdAt: z.date().describe("The folder creation date"),
|
||||
updatedAt: z.date().describe("The folder last update date"),
|
||||
totalSize: z.string().optional().describe("The total size of the folder"),
|
||||
_count: z
|
||||
.object({
|
||||
files: z.number().describe("Number of files in folder"),
|
||||
children: z.number().describe("Number of subfolders"),
|
||||
})
|
||||
.optional()
|
||||
.describe("Count statistics"),
|
||||
}),
|
||||
message: z.string().describe("Success message"),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
401: z.object({ error: z.string().describe("Error message") }),
|
||||
403: z.object({ error: z.string().describe("Error message") }),
|
||||
404: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
folderController.moveFolder.bind(folderController)
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/folders/:id",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["Folder"],
|
||||
operationId: "deleteFolder",
|
||||
summary: "Delete Folder",
|
||||
description: "Deletes a folder and all its contents",
|
||||
params: z.object({
|
||||
id: z.string().min(1, "The folder id is required").describe("The folder ID"),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
message: z.string().describe("The folder deletion message"),
|
||||
}),
|
||||
400: z.object({ error: z.string().describe("Error message") }),
|
||||
401: z.object({ error: z.string().describe("Error message") }),
|
||||
404: z.object({ error: z.string().describe("Error message") }),
|
||||
500: z.object({ error: z.string().describe("Error message") }),
|
||||
},
|
||||
},
|
||||
},
|
||||
folderController.deleteFolder.bind(folderController)
|
||||
);
|
||||
}
|
93
apps/server/src/modules/folder/service.ts
Normal file
93
apps/server/src/modules/folder/service.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { isS3Enabled } from "../../config/storage.config";
|
||||
import { FilesystemStorageProvider } from "../../providers/filesystem-storage.provider";
|
||||
import { S3StorageProvider } from "../../providers/s3-storage.provider";
|
||||
import { prisma } from "../../shared/prisma";
|
||||
import { StorageProvider } from "../../types/storage";
|
||||
|
||||
export class FolderService {
|
||||
private storageProvider: StorageProvider;
|
||||
|
||||
constructor() {
|
||||
if (isS3Enabled) {
|
||||
this.storageProvider = new S3StorageProvider();
|
||||
} else {
|
||||
this.storageProvider = FilesystemStorageProvider.getInstance();
|
||||
}
|
||||
}
|
||||
|
||||
async getPresignedPutUrl(objectName: string, expires: number): Promise<string> {
|
||||
try {
|
||||
return await this.storageProvider.getPresignedPutUrl(objectName, expires);
|
||||
} catch (err) {
|
||||
console.error("Erro no presignedPutObject:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async getPresignedGetUrl(objectName: string, expires: number, folderName?: string): Promise<string> {
|
||||
try {
|
||||
return await this.storageProvider.getPresignedGetUrl(objectName, expires, folderName);
|
||||
} catch (err) {
|
||||
console.error("Erro no presignedGetObject:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteObject(objectName: string): Promise<void> {
|
||||
try {
|
||||
await this.storageProvider.deleteObject(objectName);
|
||||
} catch (err) {
|
||||
console.error("Erro no removeObject:", err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
isFilesystemMode(): boolean {
|
||||
return !isS3Enabled;
|
||||
}
|
||||
|
||||
async getAllFilesInFolder(folderId: string, userId: string, basePath: string = ""): Promise<any[]> {
|
||||
const files = await prisma.file.findMany({
|
||||
where: { folderId, userId },
|
||||
});
|
||||
|
||||
const subfolders = await prisma.folder.findMany({
|
||||
where: { parentId: folderId, userId },
|
||||
select: { id: true, name: true },
|
||||
});
|
||||
|
||||
let allFiles = files.map((file: any) => ({
|
||||
...file,
|
||||
relativePath: basePath + file.name,
|
||||
}));
|
||||
|
||||
for (const subfolder of subfolders) {
|
||||
const subfolderPath = basePath + subfolder.name + "/";
|
||||
const subfolderFiles = await this.getAllFilesInFolder(subfolder.id, userId, subfolderPath);
|
||||
allFiles = [...allFiles, ...subfolderFiles];
|
||||
}
|
||||
|
||||
return allFiles;
|
||||
}
|
||||
|
||||
async calculateFolderSize(folderId: string, userId: string): Promise<bigint> {
|
||||
const files = await prisma.file.findMany({
|
||||
where: { folderId, userId },
|
||||
select: { size: true },
|
||||
});
|
||||
|
||||
const subfolders = await prisma.folder.findMany({
|
||||
where: { parentId: folderId, userId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
let totalSize = files.reduce((sum, file) => sum + file.size, BigInt(0));
|
||||
|
||||
for (const subfolder of subfolders) {
|
||||
const subfolderSize = await this.calculateFolderSize(subfolder.id, userId);
|
||||
totalSize += subfolderSize;
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
}
|
@@ -318,8 +318,60 @@ export class ReverseShareController {
|
||||
}
|
||||
|
||||
const { fileId } = request.params as { fileId: string };
|
||||
const result = await this.reverseShareService.downloadReverseShareFile(fileId, userId);
|
||||
return reply.send(result);
|
||||
|
||||
const fileInfo = await this.reverseShareService.getFileInfo(fileId, userId);
|
||||
const downloadId = `reverse-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
||||
|
||||
const { DownloadMemoryManager } = await import("../../utils/download-memory-manager.js");
|
||||
const memoryManager = DownloadMemoryManager.getInstance();
|
||||
|
||||
const fileSizeMB = Number(fileInfo.size) / (1024 * 1024);
|
||||
console.log(
|
||||
`[REVERSE-DOWNLOAD] Requesting slot for ${downloadId}: ${fileInfo.name} (${fileSizeMB.toFixed(1)}MB)`
|
||||
);
|
||||
|
||||
try {
|
||||
await memoryManager.requestDownloadSlot(downloadId, {
|
||||
fileName: fileInfo.name,
|
||||
fileSize: Number(fileInfo.size),
|
||||
objectName: fileInfo.objectName,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.warn(`[REVERSE-DOWNLOAD] Queued ${downloadId}: ${error.message}`);
|
||||
return reply.status(202).send({
|
||||
queued: true,
|
||||
downloadId: downloadId,
|
||||
message: "Download queued due to memory constraints",
|
||||
estimatedWaitTime: error.estimatedWaitTime || 60,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[REVERSE-DOWNLOAD] Starting ${downloadId}: ${fileInfo.name} (${fileSizeMB.toFixed(1)}MB)`);
|
||||
memoryManager.startDownload(downloadId);
|
||||
|
||||
try {
|
||||
const result = await this.reverseShareService.downloadReverseShareFile(fileId, userId);
|
||||
|
||||
const originalUrl = result.url;
|
||||
reply.header("X-Download-ID", downloadId);
|
||||
|
||||
reply.raw.on("finish", () => {
|
||||
memoryManager.endDownload(downloadId);
|
||||
});
|
||||
|
||||
reply.raw.on("close", () => {
|
||||
memoryManager.endDownload(downloadId);
|
||||
});
|
||||
|
||||
reply.raw.on("error", () => {
|
||||
memoryManager.endDownload(downloadId);
|
||||
});
|
||||
|
||||
return reply.send(result);
|
||||
} catch (downloadError) {
|
||||
memoryManager.endDownload(downloadId);
|
||||
throw downloadError;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.message === "File not found") {
|
||||
return reply.status(404).send({ error: error.message });
|
||||
|
@@ -401,6 +401,12 @@ export async function reverseShareRoutes(app: FastifyInstance) {
|
||||
url: z.string().describe("Presigned download URL - expires after 1 hour"),
|
||||
expiresIn: z.number().describe("URL expiration time in seconds (3600 = 1 hour)"),
|
||||
}),
|
||||
202: z.object({
|
||||
queued: z.boolean().describe("Download was queued due to memory constraints"),
|
||||
downloadId: z.string().describe("Download identifier for tracking"),
|
||||
message: z.string().describe("Queue status message"),
|
||||
estimatedWaitTime: z.number().describe("Estimated wait time in seconds"),
|
||||
}),
|
||||
401: z.object({ error: z.string() }),
|
||||
404: z.object({ error: z.string() }),
|
||||
},
|
||||
|
@@ -227,7 +227,7 @@ export class ReverseShareService {
|
||||
}
|
||||
}
|
||||
|
||||
const expires = 3600; // 1 hour
|
||||
const expires = parseInt(env.PRESIGNED_URL_EXPIRATION);
|
||||
const url = await this.fileService.getPresignedPutUrl(objectName, expires);
|
||||
|
||||
return { url, expiresIn: expires };
|
||||
@@ -257,7 +257,7 @@ export class ReverseShareService {
|
||||
}
|
||||
}
|
||||
|
||||
const expires = 3600; // 1 hour
|
||||
const expires = parseInt(env.PRESIGNED_URL_EXPIRATION);
|
||||
const url = await this.fileService.getPresignedPutUrl(objectName, expires);
|
||||
|
||||
return { url, expiresIn: expires };
|
||||
@@ -367,6 +367,25 @@ export class ReverseShareService {
|
||||
return this.formatFileResponse(file);
|
||||
}
|
||||
|
||||
async getFileInfo(fileId: string, creatorId: string) {
|
||||
const file = await this.reverseShareRepository.findFileById(fileId);
|
||||
if (!file) {
|
||||
throw new Error("File not found");
|
||||
}
|
||||
|
||||
if (file.reverseShare.creatorId !== creatorId) {
|
||||
throw new Error("Unauthorized to access this file");
|
||||
}
|
||||
|
||||
return {
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
objectName: file.objectName,
|
||||
extension: file.extension,
|
||||
};
|
||||
}
|
||||
|
||||
async downloadReverseShareFile(fileId: string, creatorId: string) {
|
||||
const file = await this.reverseShareRepository.findFileById(fileId);
|
||||
if (!file) {
|
||||
@@ -378,7 +397,7 @@ export class ReverseShareService {
|
||||
}
|
||||
|
||||
const fileName = file.name;
|
||||
const expires = 3600; // 1 hour
|
||||
const expires = parseInt(env.PRESIGNED_URL_EXPIRATION);
|
||||
const url = await this.fileService.getPresignedGetUrl(file.objectName, expires, fileName);
|
||||
return { url, expiresIn: expires };
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@ import { FastifyReply, FastifyRequest } from "fastify";
|
||||
|
||||
import {
|
||||
CreateShareSchema,
|
||||
UpdateShareFilesSchema,
|
||||
UpdateShareItemsSchema,
|
||||
UpdateSharePasswordSchema,
|
||||
UpdateShareRecipientsSchema,
|
||||
UpdateShareSchema,
|
||||
@@ -116,7 +116,7 @@ export class ShareController {
|
||||
}
|
||||
}
|
||||
|
||||
async addFiles(request: FastifyRequest, reply: FastifyReply) {
|
||||
async addItems(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
@@ -125,9 +125,9 @@ export class ShareController {
|
||||
}
|
||||
|
||||
const { shareId } = request.params as { shareId: string };
|
||||
const { files } = UpdateShareFilesSchema.parse(request.body);
|
||||
const { files, folders } = UpdateShareItemsSchema.parse(request.body);
|
||||
|
||||
const share = await this.shareService.addFilesToShare(shareId, userId, files);
|
||||
const share = await this.shareService.addItemsToShare(shareId, userId, files || [], folders || []);
|
||||
return reply.send({ share });
|
||||
} catch (error: any) {
|
||||
if (error.message === "Share not found") {
|
||||
@@ -136,14 +136,14 @@ export class ShareController {
|
||||
if (error.message === "Unauthorized to update this share") {
|
||||
return reply.status(401).send({ error: error.message });
|
||||
}
|
||||
if (error.message.startsWith("Files not found:")) {
|
||||
if (error.message.startsWith("Files not found:") || error.message.startsWith("Folders not found:")) {
|
||||
return reply.status(404).send({ error: error.message });
|
||||
}
|
||||
return reply.status(400).send({ error: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
async removeFiles(request: FastifyRequest, reply: FastifyReply) {
|
||||
async removeItems(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
await request.jwtVerify();
|
||||
const userId = (request as any).user?.userId;
|
||||
@@ -152,9 +152,9 @@ export class ShareController {
|
||||
}
|
||||
|
||||
const { shareId } = request.params as { shareId: string };
|
||||
const { files } = UpdateShareFilesSchema.parse(request.body);
|
||||
const { files, folders } = UpdateShareItemsSchema.parse(request.body);
|
||||
|
||||
const share = await this.shareService.removeFilesFromShare(shareId, userId, files);
|
||||
const share = await this.shareService.removeItemsFromShare(shareId, userId, files || [], folders || []);
|
||||
return reply.send({ share });
|
||||
} catch (error: any) {
|
||||
if (error.message === "Share not found") {
|
||||
|
@@ -1,19 +1,31 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const CreateShareSchema = z.object({
|
||||
name: z.string().optional().describe("The share name"),
|
||||
description: z.string().optional().describe("The share description"),
|
||||
expiration: z
|
||||
.string()
|
||||
.datetime({
|
||||
message: "Data de expiração deve estar no formato ISO 8601 (ex: 2025-02-06T13:20:49Z)",
|
||||
})
|
||||
.optional(),
|
||||
files: z.array(z.string()).describe("The file IDs"),
|
||||
password: z.string().optional().describe("The share password"),
|
||||
maxViews: z.number().optional().nullable().describe("The maximum number of views"),
|
||||
recipients: z.array(z.string().email()).optional().describe("The recipient emails"),
|
||||
});
|
||||
export const CreateShareSchema = z
|
||||
.object({
|
||||
name: z.string().optional().describe("The share name"),
|
||||
description: z.string().optional().describe("The share description"),
|
||||
expiration: z
|
||||
.string()
|
||||
.datetime({
|
||||
message: "Data de expiração deve estar no formato ISO 8601 (ex: 2025-02-06T13:20:49Z)",
|
||||
})
|
||||
.optional(),
|
||||
files: z.array(z.string()).optional().describe("The file IDs"),
|
||||
folders: z.array(z.string()).optional().describe("The folder IDs"),
|
||||
password: z.string().optional().describe("The share password"),
|
||||
maxViews: z.number().optional().nullable().describe("The maximum number of views"),
|
||||
recipients: z.array(z.string().email()).optional().describe("The recipient emails"),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const hasFiles = data.files && data.files.length > 0;
|
||||
const hasFolders = data.folders && data.folders.length > 0;
|
||||
return hasFiles || hasFolders;
|
||||
},
|
||||
{
|
||||
message: "At least one file or folder must be selected to create a share",
|
||||
}
|
||||
);
|
||||
|
||||
export const UpdateShareSchema = z.object({
|
||||
id: z.string(),
|
||||
@@ -55,10 +67,30 @@ export const ShareResponseSchema = z.object({
|
||||
size: z.string().describe("The file size"),
|
||||
objectName: z.string().describe("The file object name"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
folderId: z.string().nullable().describe("The folder ID containing this file"),
|
||||
createdAt: z.string().describe("The file creation date"),
|
||||
updatedAt: z.string().describe("The file update date"),
|
||||
})
|
||||
),
|
||||
folders: z.array(
|
||||
z.object({
|
||||
id: z.string().describe("The folder ID"),
|
||||
name: z.string().describe("The folder name"),
|
||||
description: z.string().nullable().describe("The folder description"),
|
||||
objectName: z.string().describe("The folder object name"),
|
||||
parentId: z.string().nullable().describe("The parent folder ID"),
|
||||
userId: z.string().describe("The user ID"),
|
||||
totalSize: z.string().nullable().describe("The total size of folder contents"),
|
||||
createdAt: z.string().describe("The folder creation date"),
|
||||
updatedAt: z.string().describe("The folder update date"),
|
||||
_count: z
|
||||
.object({
|
||||
files: z.number().describe("Number of files in folder"),
|
||||
children: z.number().describe("Number of subfolders"),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
),
|
||||
recipients: z.array(
|
||||
z.object({
|
||||
id: z.string().describe("The recipient ID"),
|
||||
@@ -74,9 +106,21 @@ export const UpdateSharePasswordSchema = z.object({
|
||||
password: z.string().nullable().describe("The new password. Send null to remove password"),
|
||||
});
|
||||
|
||||
export const UpdateShareFilesSchema = z.object({
|
||||
files: z.array(z.string().min(1, "File ID is required").describe("The file IDs")),
|
||||
});
|
||||
export const UpdateShareItemsSchema = z
|
||||
.object({
|
||||
files: z.array(z.string().min(1, "File ID is required").describe("The file IDs")).optional(),
|
||||
folders: z.array(z.string().min(1, "Folder ID is required").describe("The folder IDs")).optional(),
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const hasFiles = data.files && data.files.length > 0;
|
||||
const hasFolders = data.folders && data.folders.length > 0;
|
||||
return hasFiles || hasFolders;
|
||||
},
|
||||
{
|
||||
message: "At least one file or folder must be provided",
|
||||
}
|
||||
);
|
||||
|
||||
export const UpdateShareRecipientsSchema = z.object({
|
||||
emails: z.array(z.string().email("Invalid email format").describe("The recipient emails")),
|
||||
|
@@ -9,30 +9,39 @@ export interface IShareRepository {
|
||||
| (Share & {
|
||||
security: ShareSecurity;
|
||||
files: any[];
|
||||
folders: any[];
|
||||
recipients: { email: string }[];
|
||||
})
|
||||
| null
|
||||
>;
|
||||
findShareBySecurityId(securityId: string): Promise<(Share & { security: ShareSecurity; files: any[] }) | null>;
|
||||
findShareBySecurityId(
|
||||
securityId: string
|
||||
): Promise<(Share & { security: ShareSecurity; files: any[]; folders: any[] }) | null>;
|
||||
updateShare(id: string, data: Partial<Share>): Promise<Share>;
|
||||
updateShareSecurity(id: string, data: Partial<ShareSecurity>): Promise<ShareSecurity>;
|
||||
deleteShare(id: string): Promise<Share>;
|
||||
incrementViews(id: string): Promise<Share>;
|
||||
addFilesToShare(shareId: string, fileIds: string[]): Promise<void>;
|
||||
removeFilesFromShare(shareId: string, fileIds: string[]): Promise<void>;
|
||||
addFoldersToShare(shareId: string, folderIds: string[]): Promise<void>;
|
||||
removeFoldersFromShare(shareId: string, folderIds: string[]): Promise<void>;
|
||||
findFilesByIds(fileIds: string[]): Promise<any[]>;
|
||||
findFoldersByIds(folderIds: string[]): Promise<any[]>;
|
||||
addRecipients(shareId: string, emails: string[]): Promise<void>;
|
||||
removeRecipients(shareId: string, emails: string[]): Promise<void>;
|
||||
findSharesByUserId(userId: string): Promise<Share[]>;
|
||||
findSharesByUserId(
|
||||
userId: string
|
||||
): Promise<(Share & { security: ShareSecurity; files: any[]; folders: any[]; recipients: any[]; alias: any })[]>;
|
||||
}
|
||||
|
||||
export class PrismaShareRepository implements IShareRepository {
|
||||
async createShare(
|
||||
data: Omit<CreateShareInput, "password" | "maxViews"> & { securityId: string; creatorId: string }
|
||||
): Promise<Share> {
|
||||
const { files, recipients, expiration, ...shareData } = data;
|
||||
const { files, folders, recipients, expiration, ...shareData } = data;
|
||||
|
||||
const validFiles = (files ?? []).filter((id) => id && id.trim().length > 0);
|
||||
const validFolders = (folders ?? []).filter((id) => id && id.trim().length > 0);
|
||||
const validRecipients = (recipients ?? []).filter((email) => email && email.trim().length > 0);
|
||||
|
||||
return prisma.share.create({
|
||||
@@ -45,6 +54,12 @@ export class PrismaShareRepository implements IShareRepository {
|
||||
connect: validFiles.map((id) => ({ id })),
|
||||
}
|
||||
: undefined,
|
||||
folders:
|
||||
validFolders.length > 0
|
||||
? {
|
||||
connect: validFolders.map((id) => ({ id })),
|
||||
}
|
||||
: undefined,
|
||||
recipients:
|
||||
validRecipients?.length > 0
|
||||
? {
|
||||
@@ -61,10 +76,28 @@ export class PrismaShareRepository implements IShareRepository {
|
||||
return prisma.share.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
alias: true,
|
||||
security: true,
|
||||
files: true,
|
||||
folders: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
objectName: true,
|
||||
parentId: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
files: true,
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
alias: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -75,6 +108,24 @@ export class PrismaShareRepository implements IShareRepository {
|
||||
include: {
|
||||
security: true,
|
||||
files: true,
|
||||
folders: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
objectName: true,
|
||||
parentId: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
files: true,
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -121,6 +172,17 @@ export class PrismaShareRepository implements IShareRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async addFoldersToShare(shareId: string, folderIds: string[]): Promise<void> {
|
||||
await prisma.share.update({
|
||||
where: { id: shareId },
|
||||
data: {
|
||||
folders: {
|
||||
connect: folderIds.map((id) => ({ id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async removeFilesFromShare(shareId: string, fileIds: string[]): Promise<void> {
|
||||
await prisma.share.update({
|
||||
where: { id: shareId },
|
||||
@@ -132,6 +194,17 @@ export class PrismaShareRepository implements IShareRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async removeFoldersFromShare(shareId: string, folderIds: string[]): Promise<void> {
|
||||
await prisma.share.update({
|
||||
where: { id: shareId },
|
||||
data: {
|
||||
folders: {
|
||||
disconnect: folderIds.map((id) => ({ id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findFilesByIds(fileIds: string[]): Promise<any[]> {
|
||||
return prisma.file.findMany({
|
||||
where: {
|
||||
@@ -142,6 +215,16 @@ export class PrismaShareRepository implements IShareRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findFoldersByIds(folderIds: string[]): Promise<any[]> {
|
||||
return prisma.folder.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: folderIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addRecipients(shareId: string, emails: string[]): Promise<void> {
|
||||
await prisma.share.update({
|
||||
where: { id: shareId },
|
||||
@@ -178,6 +261,24 @@ export class PrismaShareRepository implements IShareRepository {
|
||||
include: {
|
||||
security: true,
|
||||
files: true,
|
||||
folders: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
objectName: true,
|
||||
parentId: true,
|
||||
userId: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
_count: {
|
||||
select: {
|
||||
files: true,
|
||||
children: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
recipients: true,
|
||||
alias: true,
|
||||
},
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
CreateShareSchema,
|
||||
ShareAliasResponseSchema,
|
||||
ShareResponseSchema,
|
||||
UpdateShareFilesSchema,
|
||||
UpdateShareItemsSchema,
|
||||
UpdateSharePasswordSchema,
|
||||
UpdateShareRecipientsSchema,
|
||||
UpdateShareSchema,
|
||||
@@ -32,7 +32,7 @@ export async function shareRoutes(app: FastifyInstance) {
|
||||
tags: ["Share"],
|
||||
operationId: "createShare",
|
||||
summary: "Create a new share",
|
||||
description: "Create a new share",
|
||||
description: "Create a new share with files and/or folders",
|
||||
body: CreateShareSchema,
|
||||
response: {
|
||||
201: z.object({
|
||||
@@ -164,17 +164,17 @@ export async function shareRoutes(app: FastifyInstance) {
|
||||
);
|
||||
|
||||
app.post(
|
||||
"/shares/:shareId/files",
|
||||
"/shares/:shareId/items",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["Share"],
|
||||
operationId: "addFiles",
|
||||
summary: "Add files to share",
|
||||
operationId: "addItems",
|
||||
summary: "Add files and/or folders to share",
|
||||
params: z.object({
|
||||
shareId: z.string().describe("The share ID"),
|
||||
}),
|
||||
body: UpdateShareFilesSchema,
|
||||
body: UpdateShareItemsSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
share: ShareResponseSchema,
|
||||
@@ -185,21 +185,21 @@ export async function shareRoutes(app: FastifyInstance) {
|
||||
},
|
||||
},
|
||||
},
|
||||
shareController.addFiles.bind(shareController)
|
||||
shareController.addItems.bind(shareController)
|
||||
);
|
||||
|
||||
app.delete(
|
||||
"/shares/:shareId/files",
|
||||
"/shares/:shareId/items",
|
||||
{
|
||||
preValidation,
|
||||
schema: {
|
||||
tags: ["Share"],
|
||||
operationId: "removeFiles",
|
||||
summary: "Remove files from share",
|
||||
operationId: "removeItems",
|
||||
summary: "Remove files and/or folders from share",
|
||||
params: z.object({
|
||||
shareId: z.string().describe("The share ID"),
|
||||
}),
|
||||
body: UpdateShareFilesSchema,
|
||||
body: UpdateShareItemsSchema,
|
||||
response: {
|
||||
200: z.object({
|
||||
share: ShareResponseSchema,
|
||||
@@ -210,7 +210,7 @@ export async function shareRoutes(app: FastifyInstance) {
|
||||
},
|
||||
},
|
||||
},
|
||||
shareController.removeFiles.bind(shareController)
|
||||
shareController.removeItems.bind(shareController)
|
||||
);
|
||||
|
||||
app.post(
|
||||
|
@@ -2,6 +2,7 @@ import bcrypt from "bcryptjs";
|
||||
|
||||
import { prisma } from "../../shared/prisma";
|
||||
import { EmailService } from "../email/service";
|
||||
import { FolderService } from "../folder/service";
|
||||
import { UserService } from "../user/service";
|
||||
import { CreateShareInput, ShareResponseSchema, UpdateShareInput } from "./dto";
|
||||
import { IShareRepository, PrismaShareRepository } from "./repository";
|
||||
@@ -11,8 +12,9 @@ export class ShareService {
|
||||
|
||||
private emailService = new EmailService();
|
||||
private userService = new UserService();
|
||||
private folderService = new FolderService();
|
||||
|
||||
private formatShareResponse(share: any) {
|
||||
private async formatShareResponse(share: any) {
|
||||
return {
|
||||
...share,
|
||||
createdAt: share.createdAt.toISOString(),
|
||||
@@ -36,6 +38,20 @@ export class ShareService {
|
||||
createdAt: file.createdAt.toISOString(),
|
||||
updatedAt: file.updatedAt.toISOString(),
|
||||
})) || [],
|
||||
folders:
|
||||
share.folders && share.folders.length > 0
|
||||
? await Promise.all(
|
||||
share.folders.map(async (folder: any) => {
|
||||
const totalSize = await this.folderService.calculateFolderSize(folder.id, folder.userId);
|
||||
return {
|
||||
...folder,
|
||||
totalSize: totalSize.toString(),
|
||||
createdAt: folder.createdAt.toISOString(),
|
||||
updatedAt: folder.updatedAt.toISOString(),
|
||||
};
|
||||
})
|
||||
)
|
||||
: [],
|
||||
recipients:
|
||||
share.recipients?.map((recipient: any) => ({
|
||||
...recipient,
|
||||
@@ -46,7 +62,37 @@ export class ShareService {
|
||||
}
|
||||
|
||||
async createShare(data: CreateShareInput, userId: string) {
|
||||
const { password, maxViews, ...shareData } = data;
|
||||
const { password, maxViews, files, folders, ...shareData } = data;
|
||||
|
||||
if (files && files.length > 0) {
|
||||
const existingFiles = await prisma.file.findMany({
|
||||
where: {
|
||||
id: { in: files },
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
const notFoundFiles = files.filter((id) => !existingFiles.some((file) => file.id === id));
|
||||
if (notFoundFiles.length > 0) {
|
||||
throw new Error(`Files not found or access denied: ${notFoundFiles.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (folders && folders.length > 0) {
|
||||
const existingFolders = await prisma.folder.findMany({
|
||||
where: {
|
||||
id: { in: folders },
|
||||
userId: userId,
|
||||
},
|
||||
});
|
||||
const notFoundFolders = folders.filter((id) => !existingFolders.some((folder) => folder.id === id));
|
||||
if (notFoundFolders.length > 0) {
|
||||
throw new Error(`Folders not found or access denied: ${notFoundFolders.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
if ((!files || files.length === 0) && (!folders || folders.length === 0)) {
|
||||
throw new Error("At least one file or folder must be selected to create a share");
|
||||
}
|
||||
|
||||
const security = await prisma.shareSecurity.create({
|
||||
data: {
|
||||
@@ -57,12 +103,14 @@ export class ShareService {
|
||||
|
||||
const share = await this.shareRepository.createShare({
|
||||
...shareData,
|
||||
files,
|
||||
folders,
|
||||
securityId: security.id,
|
||||
creatorId: userId,
|
||||
});
|
||||
|
||||
const shareWithRelations = await this.shareRepository.findShareById(share.id);
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(shareWithRelations));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(shareWithRelations));
|
||||
}
|
||||
|
||||
async getShare(shareId: string, password?: string, userId?: string) {
|
||||
@@ -73,7 +121,7 @@ export class ShareService {
|
||||
}
|
||||
|
||||
if (userId && share.creatorId === userId) {
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(share));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(share));
|
||||
}
|
||||
|
||||
if (share.expiration && new Date() > new Date(share.expiration)) {
|
||||
@@ -98,7 +146,7 @@ export class ShareService {
|
||||
await this.shareRepository.incrementViews(shareId);
|
||||
|
||||
const updatedShare = await this.shareRepository.findShareById(shareId);
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(updatedShare));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(updatedShare));
|
||||
}
|
||||
|
||||
async updateShare(shareId: string, data: Omit<UpdateShareInput, "id">, userId: string) {
|
||||
@@ -136,7 +184,7 @@ export class ShareService {
|
||||
});
|
||||
const shareWithRelations = await this.shareRepository.findShareById(shareId);
|
||||
|
||||
return this.formatShareResponse(shareWithRelations);
|
||||
return await this.formatShareResponse(shareWithRelations);
|
||||
}
|
||||
|
||||
async deleteShare(id: string) {
|
||||
@@ -172,12 +220,12 @@ export class ShareService {
|
||||
return deletedShare;
|
||||
});
|
||||
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(deleted));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(deleted));
|
||||
}
|
||||
|
||||
async listUserShares(userId: string) {
|
||||
const shares = await this.shareRepository.findSharesByUserId(userId);
|
||||
return shares.map((share) => this.formatShareResponse(share));
|
||||
return await Promise.all(shares.map(async (share) => await this.formatShareResponse(share)));
|
||||
}
|
||||
|
||||
async updateSharePassword(shareId: string, userId: string, password: string | null) {
|
||||
@@ -195,10 +243,10 @@ export class ShareService {
|
||||
});
|
||||
|
||||
const updated = await this.shareRepository.findShareById(shareId);
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(updated));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(updated));
|
||||
}
|
||||
|
||||
async addFilesToShare(shareId: string, userId: string, fileIds: string[]) {
|
||||
async addItemsToShare(shareId: string, userId: string, fileIds: string[], folderIds: string[]) {
|
||||
const share = await this.shareRepository.findShareById(shareId);
|
||||
if (!share) {
|
||||
throw new Error("Share not found");
|
||||
@@ -208,19 +256,33 @@ export class ShareService {
|
||||
throw new Error("Unauthorized to update this share");
|
||||
}
|
||||
|
||||
const existingFiles = await this.shareRepository.findFilesByIds(fileIds);
|
||||
const notFoundFiles = fileIds.filter((id) => !existingFiles.some((file) => file.id === id));
|
||||
if (fileIds.length > 0) {
|
||||
const existingFiles = await this.shareRepository.findFilesByIds(fileIds);
|
||||
const notFoundFiles = fileIds.filter((id) => !existingFiles.some((file) => file.id === id));
|
||||
|
||||
if (notFoundFiles.length > 0) {
|
||||
throw new Error(`Files not found: ${notFoundFiles.join(", ")}`);
|
||||
if (notFoundFiles.length > 0) {
|
||||
throw new Error(`Files not found: ${notFoundFiles.join(", ")}`);
|
||||
}
|
||||
|
||||
await this.shareRepository.addFilesToShare(shareId, fileIds);
|
||||
}
|
||||
|
||||
if (folderIds.length > 0) {
|
||||
const existingFolders = await this.shareRepository.findFoldersByIds(folderIds);
|
||||
const notFoundFolders = folderIds.filter((id) => !existingFolders.some((folder) => folder.id === id));
|
||||
|
||||
if (notFoundFolders.length > 0) {
|
||||
throw new Error(`Folders not found: ${notFoundFolders.join(", ")}`);
|
||||
}
|
||||
|
||||
await this.shareRepository.addFoldersToShare(shareId, folderIds);
|
||||
}
|
||||
|
||||
await this.shareRepository.addFilesToShare(shareId, fileIds);
|
||||
const updated = await this.shareRepository.findShareById(shareId);
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(updated));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(updated));
|
||||
}
|
||||
|
||||
async removeFilesFromShare(shareId: string, userId: string, fileIds: string[]) {
|
||||
async removeItemsFromShare(shareId: string, userId: string, fileIds: string[], folderIds: string[]) {
|
||||
const share = await this.shareRepository.findShareById(shareId);
|
||||
if (!share) {
|
||||
throw new Error("Share not found");
|
||||
@@ -230,9 +292,16 @@ export class ShareService {
|
||||
throw new Error("Unauthorized to update this share");
|
||||
}
|
||||
|
||||
await this.shareRepository.removeFilesFromShare(shareId, fileIds);
|
||||
if (fileIds.length > 0) {
|
||||
await this.shareRepository.removeFilesFromShare(shareId, fileIds);
|
||||
}
|
||||
|
||||
if (folderIds.length > 0) {
|
||||
await this.shareRepository.removeFoldersFromShare(shareId, folderIds);
|
||||
}
|
||||
|
||||
const updated = await this.shareRepository.findShareById(shareId);
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(updated));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(updated));
|
||||
}
|
||||
|
||||
async findShareById(id: string) {
|
||||
@@ -255,7 +324,7 @@ export class ShareService {
|
||||
|
||||
await this.shareRepository.addRecipients(shareId, emails);
|
||||
const updated = await this.shareRepository.findShareById(shareId);
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(updated));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(updated));
|
||||
}
|
||||
|
||||
async removeRecipients(shareId: string, userId: string, emails: string[]) {
|
||||
@@ -270,7 +339,7 @@ export class ShareService {
|
||||
|
||||
await this.shareRepository.removeRecipients(shareId, emails);
|
||||
const updated = await this.shareRepository.findShareById(shareId);
|
||||
return ShareResponseSchema.parse(this.formatShareResponse(updated));
|
||||
return ShareResponseSchema.parse(await this.formatShareResponse(updated));
|
||||
}
|
||||
|
||||
async createOrUpdateAlias(shareId: string, alias: string, userId: string) {
|
||||
@@ -341,7 +410,6 @@ export class ShareService {
|
||||
throw new Error("No recipients found for this share");
|
||||
}
|
||||
|
||||
// Get sender information
|
||||
let senderName = "Someone";
|
||||
try {
|
||||
const sender = await this.userService.getUserById(userId);
|
||||
|
@@ -284,7 +284,7 @@ export class StorageService {
|
||||
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
|
||||
const basePaths = IS_RUNNING_IN_CONTAINER
|
||||
? ["/app/server/uploads", "/app/server/temp-uploads", "/app/server/temp-chunks", "/app/server", "/app", "/"]
|
||||
: [".", "./uploads", process.cwd()];
|
||||
: [env.CUSTOM_PATH || ".", "./uploads", process.cwd()];
|
||||
|
||||
const synologyPaths = await this._detectSynologyVolumes();
|
||||
|
||||
|
@@ -80,7 +80,8 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
public createEncryptStream(): Transform {
|
||||
if (this.isEncryptionDisabled) {
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
highWaterMark: 64 * 1024,
|
||||
transform(chunk, _encoding, callback) {
|
||||
this.push(chunk);
|
||||
callback();
|
||||
},
|
||||
@@ -94,7 +95,8 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
let isFirstChunk = true;
|
||||
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
highWaterMark: 64 * 1024,
|
||||
transform(chunk, _encoding, callback) {
|
||||
try {
|
||||
if (isFirstChunk) {
|
||||
this.push(iv);
|
||||
@@ -124,7 +126,8 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
public createDecryptStream(): Transform {
|
||||
if (this.isEncryptionDisabled) {
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
highWaterMark: 64 * 1024,
|
||||
transform(chunk, _encoding, callback) {
|
||||
this.push(chunk);
|
||||
callback();
|
||||
},
|
||||
@@ -137,15 +140,16 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
let ivBuffer = Buffer.alloc(0);
|
||||
|
||||
return new Transform({
|
||||
transform(chunk, encoding, callback) {
|
||||
highWaterMark: 64 * 1024,
|
||||
transform(chunk, _encoding, callback) {
|
||||
try {
|
||||
if (!iv) {
|
||||
ivBuffer = Buffer.concat([ivBuffer, chunk]);
|
||||
|
||||
if (ivBuffer.length >= 16) {
|
||||
iv = ivBuffer.slice(0, 16);
|
||||
iv = ivBuffer.subarray(0, 16);
|
||||
decipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
|
||||
const remainingData = ivBuffer.slice(16);
|
||||
const remainingData = ivBuffer.subarray(16);
|
||||
if (remainingData.length > 0) {
|
||||
const decrypted = decipher.update(remainingData);
|
||||
this.push(decrypted);
|
||||
@@ -236,7 +240,7 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
|
||||
try {
|
||||
await pipeline(inputStream, encryptStream, writeStream);
|
||||
await fs.rename(tempPath, filePath);
|
||||
await this.moveFile(tempPath, filePath);
|
||||
} catch (error) {
|
||||
await this.cleanupTempFile(tempPath);
|
||||
throw error;
|
||||
@@ -267,31 +271,35 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
|
||||
createDownloadStream(objectName: string): NodeJS.ReadableStream {
|
||||
const filePath = this.getFilePath(objectName);
|
||||
const fileStream = fsSync.createReadStream(filePath);
|
||||
|
||||
const streamOptions = {
|
||||
highWaterMark: 64 * 1024,
|
||||
autoDestroy: true,
|
||||
emitClose: true,
|
||||
};
|
||||
|
||||
const fileStream = fsSync.createReadStream(filePath, streamOptions);
|
||||
|
||||
if (this.isEncryptionDisabled) {
|
||||
fileStream.on("end", () => {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
});
|
||||
|
||||
fileStream.on("close", () => {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
});
|
||||
|
||||
this.setupStreamMemoryManagement(fileStream, objectName);
|
||||
return fileStream;
|
||||
}
|
||||
|
||||
const decryptStream = this.createDecryptStream();
|
||||
const { PassThrough } = require("stream");
|
||||
const outputStream = new PassThrough(streamOptions);
|
||||
|
||||
let isDestroyed = false;
|
||||
let memoryCheckInterval: NodeJS.Timeout;
|
||||
|
||||
const cleanup = () => {
|
||||
if (isDestroyed) return;
|
||||
isDestroyed = true;
|
||||
|
||||
if (memoryCheckInterval) {
|
||||
clearInterval(memoryCheckInterval);
|
||||
}
|
||||
|
||||
try {
|
||||
if (fileStream && !fileStream.destroyed) {
|
||||
fileStream.destroy();
|
||||
@@ -299,28 +307,104 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
if (decryptStream && !decryptStream.destroyed) {
|
||||
decryptStream.destroy();
|
||||
}
|
||||
if (outputStream && !outputStream.destroyed) {
|
||||
outputStream.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error during download stream cleanup:", error);
|
||||
}
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
setImmediate(() => {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fileStream.on("error", cleanup);
|
||||
decryptStream.on("error", cleanup);
|
||||
decryptStream.on("end", cleanup);
|
||||
decryptStream.on("close", cleanup);
|
||||
memoryCheckInterval = setInterval(() => {
|
||||
const memUsage = process.memoryUsage();
|
||||
const memoryUsageMB = memUsage.heapUsed / 1024 / 1024;
|
||||
|
||||
decryptStream.on("pipe", (src: any) => {
|
||||
if (memoryUsageMB > 1024) {
|
||||
if (!fileStream.readableFlowing) return;
|
||||
|
||||
console.warn(
|
||||
`[MEMORY THROTTLE] ${objectName} - Pausing stream due to high memory usage: ${memoryUsageMB.toFixed(2)}MB`
|
||||
);
|
||||
fileStream.pause();
|
||||
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (!isDestroyed && fileStream && !fileStream.destroyed) {
|
||||
fileStream.resume();
|
||||
console.log(`[MEMORY THROTTLE] ${objectName} - Stream resumed`);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
fileStream.on("error", (error: any) => {
|
||||
console.error("File stream error:", error);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
decryptStream.on("error", (error: any) => {
|
||||
console.error("Decrypt stream error:", error);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
outputStream.on("error", (error: any) => {
|
||||
console.error("Output stream error:", error);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
outputStream.on("close", cleanup);
|
||||
outputStream.on("finish", cleanup);
|
||||
|
||||
outputStream.on("pipe", (src: any) => {
|
||||
if (src && src.on) {
|
||||
src.on("close", cleanup);
|
||||
src.on("error", cleanup);
|
||||
}
|
||||
});
|
||||
|
||||
return fileStream.pipe(decryptStream);
|
||||
pipeline(fileStream, decryptStream, outputStream)
|
||||
.then(() => {})
|
||||
.catch((error: any) => {
|
||||
console.error("Pipeline error during download:", error);
|
||||
cleanup();
|
||||
});
|
||||
|
||||
this.setupStreamMemoryManagement(outputStream, objectName);
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
private setupStreamMemoryManagement(stream: NodeJS.ReadableStream, objectName: string): void {
|
||||
let lastMemoryLog = 0;
|
||||
|
||||
stream.on("data", () => {
|
||||
const now = Date.now();
|
||||
if (now - lastMemoryLog > 30000) {
|
||||
FilesystemStorageProvider.logMemoryUsage(`Active download: ${objectName}`);
|
||||
lastMemoryLog = now;
|
||||
}
|
||||
});
|
||||
|
||||
stream.on("end", () => {
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download completed: ${objectName}`);
|
||||
setImmediate(() => {
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
stream.on("close", () => {
|
||||
FilesystemStorageProvider.logMemoryUsage(`Download closed: ${objectName}`);
|
||||
});
|
||||
}
|
||||
|
||||
async createDownloadRangeStream(objectName: string, start: number, end: number): Promise<NodeJS.ReadableStream> {
|
||||
@@ -623,4 +707,18 @@ export class FilesystemStorageProvider implements StorageProvider {
|
||||
console.error("Error during temp directory cleanup:", error);
|
||||
}
|
||||
}
|
||||
|
||||
private async moveFile(src: string, dest: string): Promise<void> {
|
||||
try {
|
||||
await fs.rename(src, dest);
|
||||
} catch (err: any) {
|
||||
if (err.code === "EXDEV") {
|
||||
// cross-device: fallback to copy + delete
|
||||
await fs.copyFile(src, dest);
|
||||
await fs.unlink(src);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
|
||||
|
||||
import { bucketName, s3Client } from "../config/storage.config";
|
||||
import { StorageProvider } from "../types/storage";
|
||||
import { getContentType } from "../utils/mime-types";
|
||||
|
||||
export class S3StorageProvider implements StorageProvider {
|
||||
constructor() {
|
||||
@@ -91,6 +92,7 @@ export class S3StorageProvider implements StorageProvider {
|
||||
Bucket: bucketName,
|
||||
Key: objectName,
|
||||
ResponseContentDisposition: this.encodeFilenameForHeader(rcdFileName),
|
||||
ResponseContentType: getContentType(rcdFileName),
|
||||
});
|
||||
|
||||
return await getSignedUrl(s3Client, command, { expiresIn: expires });
|
||||
|
@@ -12,7 +12,9 @@ import { authProvidersRoutes } from "./modules/auth-providers/routes";
|
||||
import { authRoutes } from "./modules/auth/routes";
|
||||
import { fileRoutes } from "./modules/file/routes";
|
||||
import { ChunkManager } from "./modules/filesystem/chunk-manager";
|
||||
import { downloadQueueRoutes } from "./modules/filesystem/download-queue-routes";
|
||||
import { filesystemRoutes } from "./modules/filesystem/routes";
|
||||
import { folderRoutes } from "./modules/folder/routes";
|
||||
import { healthRoutes } from "./modules/health/routes";
|
||||
import { reverseShareRoutes } from "./modules/reverse-share/routes";
|
||||
import { shareRoutes } from "./modules/share/routes";
|
||||
@@ -74,17 +76,18 @@ async function startServer() {
|
||||
app.register(twoFactorRoutes, { prefix: "/auth" });
|
||||
app.register(userRoutes);
|
||||
app.register(fileRoutes);
|
||||
|
||||
if (env.ENABLE_S3 !== "true") {
|
||||
app.register(filesystemRoutes);
|
||||
}
|
||||
|
||||
app.register(folderRoutes);
|
||||
app.register(downloadQueueRoutes);
|
||||
app.register(shareRoutes);
|
||||
app.register(reverseShareRoutes);
|
||||
app.register(storageRoutes);
|
||||
app.register(appRoutes);
|
||||
app.register(healthRoutes);
|
||||
|
||||
if (env.ENABLE_S3 !== "true") {
|
||||
app.register(filesystemRoutes);
|
||||
}
|
||||
|
||||
await app.listen({
|
||||
port: 3333,
|
||||
host: "0.0.0.0",
|
||||
|
52
apps/server/src/types/download-queue.ts
Normal file
52
apps/server/src/types/download-queue.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* TypeScript interfaces for download queue management
|
||||
*/
|
||||
|
||||
export interface QueuedDownloadInfo {
|
||||
downloadId: string;
|
||||
position: number;
|
||||
waitTime: number;
|
||||
fileName?: string;
|
||||
fileSize?: number;
|
||||
}
|
||||
|
||||
export interface QueueStatus {
|
||||
queueLength: number;
|
||||
maxQueueSize: number;
|
||||
activeDownloads: number;
|
||||
maxConcurrent: number;
|
||||
queuedDownloads: QueuedDownloadInfo[];
|
||||
}
|
||||
|
||||
export interface DownloadCancelResponse {
|
||||
message: string;
|
||||
downloadId: string;
|
||||
}
|
||||
|
||||
export interface QueueClearResponse {
|
||||
message: string;
|
||||
clearedCount: number;
|
||||
}
|
||||
|
||||
export interface ApiResponse<T = any> {
|
||||
status: "success" | "error";
|
||||
data?: T;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface QueueStatusResponse extends ApiResponse<QueueStatus> {
|
||||
status: "success";
|
||||
data: QueueStatus;
|
||||
}
|
||||
|
||||
export interface DownloadSlotRequest {
|
||||
fileName?: string;
|
||||
fileSize?: number;
|
||||
objectName: string;
|
||||
}
|
||||
|
||||
export interface ActiveDownloadInfo {
|
||||
startTime: number;
|
||||
memoryAtStart: number;
|
||||
}
|
423
apps/server/src/utils/download-memory-manager.ts
Normal file
423
apps/server/src/utils/download-memory-manager.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
import { ActiveDownloadInfo, DownloadSlotRequest, QueuedDownloadInfo, QueueStatus } from "../types/download-queue";
|
||||
|
||||
interface QueuedDownload {
|
||||
downloadId: string;
|
||||
queuedAt: number;
|
||||
resolve: () => void;
|
||||
reject: (error: Error) => void;
|
||||
metadata?: DownloadSlotRequest;
|
||||
}
|
||||
|
||||
export class DownloadMemoryManager {
|
||||
private static instance: DownloadMemoryManager;
|
||||
private activeDownloads = new Map<string, ActiveDownloadInfo>();
|
||||
private downloadQueue: QueuedDownload[] = [];
|
||||
private maxConcurrentDownloads: number;
|
||||
private memoryThresholdMB: number;
|
||||
private maxQueueSize: number;
|
||||
private cleanupInterval: NodeJS.Timeout;
|
||||
private isAutoScalingEnabled: boolean;
|
||||
private minFileSizeGB: number;
|
||||
|
||||
private constructor() {
|
||||
const { env } = require("../env");
|
||||
|
||||
const totalMemoryGB = require("os").totalmem() / 1024 ** 3;
|
||||
this.isAutoScalingEnabled = env.DOWNLOAD_AUTO_SCALE === "true";
|
||||
|
||||
if (env.DOWNLOAD_MAX_CONCURRENT !== undefined) {
|
||||
this.maxConcurrentDownloads = env.DOWNLOAD_MAX_CONCURRENT;
|
||||
} else if (this.isAutoScalingEnabled) {
|
||||
this.maxConcurrentDownloads = this.calculateDefaultConcurrentDownloads(totalMemoryGB);
|
||||
} else {
|
||||
this.maxConcurrentDownloads = 3;
|
||||
}
|
||||
|
||||
if (env.DOWNLOAD_MEMORY_THRESHOLD_MB !== undefined) {
|
||||
this.memoryThresholdMB = env.DOWNLOAD_MEMORY_THRESHOLD_MB;
|
||||
} else if (this.isAutoScalingEnabled) {
|
||||
this.memoryThresholdMB = this.calculateDefaultMemoryThreshold(totalMemoryGB);
|
||||
} else {
|
||||
this.memoryThresholdMB = 1024;
|
||||
}
|
||||
|
||||
if (env.DOWNLOAD_QUEUE_SIZE !== undefined) {
|
||||
this.maxQueueSize = env.DOWNLOAD_QUEUE_SIZE;
|
||||
} else if (this.isAutoScalingEnabled) {
|
||||
this.maxQueueSize = this.calculateDefaultQueueSize(totalMemoryGB);
|
||||
} else {
|
||||
this.maxQueueSize = 15;
|
||||
}
|
||||
|
||||
if (env.DOWNLOAD_MIN_FILE_SIZE_GB !== undefined) {
|
||||
this.minFileSizeGB = env.DOWNLOAD_MIN_FILE_SIZE_GB;
|
||||
} else {
|
||||
this.minFileSizeGB = 3.0;
|
||||
}
|
||||
|
||||
this.validateConfiguration();
|
||||
|
||||
console.log(`[DOWNLOAD MANAGER] Configuration loaded:`);
|
||||
console.log(` System Memory: ${totalMemoryGB.toFixed(1)}GB`);
|
||||
console.log(
|
||||
` Max Concurrent: ${this.maxConcurrentDownloads} ${env.DOWNLOAD_MAX_CONCURRENT !== undefined ? "(ENV)" : "(AUTO)"}`
|
||||
);
|
||||
console.log(
|
||||
` Memory Threshold: ${this.memoryThresholdMB}MB ${env.DOWNLOAD_MEMORY_THRESHOLD_MB !== undefined ? "(ENV)" : "(AUTO)"}`
|
||||
);
|
||||
console.log(` Queue Size: ${this.maxQueueSize} ${env.DOWNLOAD_QUEUE_SIZE !== undefined ? "(ENV)" : "(AUTO)"}`);
|
||||
console.log(
|
||||
` Min File Size: ${this.minFileSizeGB}GB ${env.DOWNLOAD_MIN_FILE_SIZE_GB !== undefined ? "(ENV)" : "(DEFAULT)"}`
|
||||
);
|
||||
console.log(` Auto-scaling: ${this.isAutoScalingEnabled ? "enabled" : "disabled"}`);
|
||||
|
||||
this.cleanupInterval = setInterval(() => {
|
||||
this.cleanupStaleDownloads();
|
||||
}, 30000);
|
||||
}
|
||||
|
||||
public static getInstance(): DownloadMemoryManager {
|
||||
if (!DownloadMemoryManager.instance) {
|
||||
DownloadMemoryManager.instance = new DownloadMemoryManager();
|
||||
}
|
||||
return DownloadMemoryManager.instance;
|
||||
}
|
||||
|
||||
private calculateDefaultConcurrentDownloads(totalMemoryGB: number): number {
|
||||
if (totalMemoryGB > 16) return 10;
|
||||
if (totalMemoryGB > 8) return 5;
|
||||
if (totalMemoryGB > 4) return 3;
|
||||
if (totalMemoryGB > 2) return 2;
|
||||
return 1;
|
||||
}
|
||||
|
||||
private calculateDefaultMemoryThreshold(totalMemoryGB: number): number {
|
||||
if (totalMemoryGB > 16) return 4096; // 4GB
|
||||
if (totalMemoryGB > 8) return 2048; // 2GB
|
||||
if (totalMemoryGB > 4) return 1024; // 1GB
|
||||
if (totalMemoryGB > 2) return 512; // 512MB
|
||||
return 256; // 256MB
|
||||
}
|
||||
|
||||
private calculateDefaultQueueSize(totalMemoryGB: number): number {
|
||||
if (totalMemoryGB > 16) return 50; // Large queue for powerful servers
|
||||
if (totalMemoryGB > 8) return 25; // Medium queue
|
||||
if (totalMemoryGB > 4) return 15; // Small queue
|
||||
if (totalMemoryGB > 2) return 10; // Very small queue
|
||||
return 5; // Minimal queue
|
||||
}
|
||||
|
||||
private validateConfiguration(): void {
|
||||
const warnings: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
if (this.maxConcurrentDownloads < 1) {
|
||||
errors.push(`DOWNLOAD_MAX_CONCURRENT must be >= 1, got: ${this.maxConcurrentDownloads}`);
|
||||
}
|
||||
if (this.maxConcurrentDownloads > 50) {
|
||||
warnings.push(
|
||||
`DOWNLOAD_MAX_CONCURRENT is very high (${this.maxConcurrentDownloads}), this may cause performance issues`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.memoryThresholdMB < 128) {
|
||||
warnings.push(
|
||||
`DOWNLOAD_MEMORY_THRESHOLD_MB is very low (${this.memoryThresholdMB}MB), downloads may be throttled frequently`
|
||||
);
|
||||
}
|
||||
if (this.memoryThresholdMB > 16384) {
|
||||
warnings.push(
|
||||
`DOWNLOAD_MEMORY_THRESHOLD_MB is very high (${this.memoryThresholdMB}MB), system may run out of memory`
|
||||
);
|
||||
}
|
||||
|
||||
if (this.maxQueueSize < 1) {
|
||||
errors.push(`DOWNLOAD_QUEUE_SIZE must be >= 1, got: ${this.maxQueueSize}`);
|
||||
}
|
||||
if (this.maxQueueSize > 1000) {
|
||||
warnings.push(`DOWNLOAD_QUEUE_SIZE is very high (${this.maxQueueSize}), this may consume significant memory`);
|
||||
}
|
||||
|
||||
if (this.minFileSizeGB < 0.1) {
|
||||
warnings.push(
|
||||
`DOWNLOAD_MIN_FILE_SIZE_GB is very low (${this.minFileSizeGB}GB), most downloads will use memory management`
|
||||
);
|
||||
}
|
||||
if (this.minFileSizeGB > 50) {
|
||||
warnings.push(
|
||||
`DOWNLOAD_MIN_FILE_SIZE_GB is very high (${this.minFileSizeGB}GB), memory management may rarely activate`
|
||||
);
|
||||
}
|
||||
|
||||
const recommendedQueueSize = this.maxConcurrentDownloads * 5;
|
||||
if (this.maxQueueSize < this.maxConcurrentDownloads) {
|
||||
warnings.push(
|
||||
`DOWNLOAD_QUEUE_SIZE (${this.maxQueueSize}) is smaller than DOWNLOAD_MAX_CONCURRENT (${this.maxConcurrentDownloads})`
|
||||
);
|
||||
} else if (this.maxQueueSize < recommendedQueueSize) {
|
||||
warnings.push(
|
||||
`DOWNLOAD_QUEUE_SIZE (${this.maxQueueSize}) might be too small. Recommended: ${recommendedQueueSize} (5x concurrent downloads)`
|
||||
);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
console.warn(`[DOWNLOAD MANAGER] Configuration warnings:`);
|
||||
warnings.forEach((warning) => console.warn(` - ${warning}`));
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`[DOWNLOAD MANAGER] Configuration errors:`);
|
||||
errors.forEach((error) => console.error(` - ${error}`));
|
||||
throw new Error(`Invalid download manager configuration: ${errors.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async requestDownloadSlot(downloadId: string, metadata?: DownloadSlotRequest): Promise<void> {
|
||||
if (metadata?.fileSize) {
|
||||
const fileSizeGB = metadata.fileSize / 1024 ** 3;
|
||||
if (fileSizeGB < this.minFileSizeGB) {
|
||||
console.log(
|
||||
`[DOWNLOAD MANAGER] File ${metadata.fileName || "unknown"} (${fileSizeGB.toFixed(2)}GB) below threshold (${this.minFileSizeGB}GB), bypassing queue`
|
||||
);
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.canStartImmediately()) {
|
||||
console.log(`[DOWNLOAD MANAGER] Immediate start: ${downloadId}`);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (this.downloadQueue.length >= this.maxQueueSize) {
|
||||
const error = new Error(`Download queue is full: ${this.downloadQueue.length}/${this.maxQueueSize}`);
|
||||
throw error;
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const queuedDownload: QueuedDownload = {
|
||||
downloadId,
|
||||
queuedAt: Date.now(),
|
||||
resolve,
|
||||
reject,
|
||||
metadata,
|
||||
};
|
||||
|
||||
this.downloadQueue.push(queuedDownload);
|
||||
|
||||
const position = this.downloadQueue.length;
|
||||
console.log(`[DOWNLOAD MANAGER] Queued: ${downloadId} (Position: ${position}/${this.maxQueueSize})`);
|
||||
|
||||
if (metadata?.fileName && metadata?.fileSize) {
|
||||
const sizeMB = (metadata.fileSize / (1024 * 1024)).toFixed(1);
|
||||
console.log(`[DOWNLOAD MANAGER] Queued file: ${metadata.fileName} (${sizeMB}MB)`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private canStartImmediately(): boolean {
|
||||
const currentMemoryMB = this.getCurrentMemoryUsage();
|
||||
|
||||
if (currentMemoryMB > this.memoryThresholdMB) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.activeDownloads.size >= this.maxConcurrentDownloads) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public canStartDownload(): { allowed: boolean; reason?: string } {
|
||||
if (this.canStartImmediately()) {
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
const currentMemoryMB = this.getCurrentMemoryUsage();
|
||||
|
||||
if (currentMemoryMB > this.memoryThresholdMB) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Memory usage too high: ${currentMemoryMB.toFixed(0)}MB > ${this.memoryThresholdMB}MB`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
allowed: false,
|
||||
reason: `Too many concurrent downloads: ${this.activeDownloads.size}/${this.maxConcurrentDownloads}`,
|
||||
};
|
||||
}
|
||||
|
||||
public startDownload(downloadId: string): void {
|
||||
const memUsage = process.memoryUsage();
|
||||
this.activeDownloads.set(downloadId, {
|
||||
startTime: Date.now(),
|
||||
memoryAtStart: memUsage.rss + memUsage.external,
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[DOWNLOAD MANAGER] Started: ${downloadId} (${this.activeDownloads.size}/${this.maxConcurrentDownloads} active)`
|
||||
);
|
||||
}
|
||||
|
||||
public endDownload(downloadId: string): void {
|
||||
const downloadInfo = this.activeDownloads.get(downloadId);
|
||||
this.activeDownloads.delete(downloadId);
|
||||
|
||||
if (downloadInfo) {
|
||||
const duration = Date.now() - downloadInfo.startTime;
|
||||
const memUsage = process.memoryUsage();
|
||||
const currentMemory = memUsage.rss + memUsage.external;
|
||||
const memoryDiff = currentMemory - downloadInfo.memoryAtStart;
|
||||
|
||||
console.log(
|
||||
`[DOWNLOAD MANAGER] Ended: ${downloadId} (Duration: ${(duration / 1000).toFixed(1)}s, Memory delta: ${(memoryDiff / 1024 / 1024).toFixed(1)}MB)`
|
||||
);
|
||||
|
||||
if (memoryDiff > 100 * 1024 * 1024 && global.gc) {
|
||||
setImmediate(() => {
|
||||
global.gc!();
|
||||
console.log(`[DOWNLOAD MANAGER] Forced GC after download ${downloadId}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
private processQueue(): void {
|
||||
if (this.downloadQueue.length === 0 || !this.canStartImmediately()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextDownload = this.downloadQueue.shift();
|
||||
if (!nextDownload) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
`[DOWNLOAD MANAGER] Processing queue: ${nextDownload.downloadId} (${this.downloadQueue.length} remaining)`
|
||||
);
|
||||
|
||||
if (nextDownload.metadata?.fileName && nextDownload.metadata?.fileSize) {
|
||||
const sizeMB = (nextDownload.metadata.fileSize / (1024 * 1024)).toFixed(1);
|
||||
console.log(`[DOWNLOAD MANAGER] Starting queued file: ${nextDownload.metadata.fileName} (${sizeMB}MB)`);
|
||||
}
|
||||
|
||||
nextDownload.resolve();
|
||||
}
|
||||
|
||||
public getActiveDownloadsCount(): number {
|
||||
return this.activeDownloads.size;
|
||||
}
|
||||
|
||||
private getCurrentMemoryUsage(): number {
|
||||
const usage = process.memoryUsage();
|
||||
return (usage.rss + usage.external) / (1024 * 1024);
|
||||
}
|
||||
|
||||
public getCurrentMemoryUsageMB(): number {
|
||||
return this.getCurrentMemoryUsage();
|
||||
}
|
||||
|
||||
public getQueueStatus(): QueueStatus {
|
||||
return {
|
||||
queueLength: this.downloadQueue.length,
|
||||
maxQueueSize: this.maxQueueSize,
|
||||
activeDownloads: this.activeDownloads.size,
|
||||
maxConcurrent: this.maxConcurrentDownloads,
|
||||
queuedDownloads: this.downloadQueue.map((download, index) => ({
|
||||
downloadId: download.downloadId,
|
||||
position: index + 1,
|
||||
waitTime: Date.now() - download.queuedAt,
|
||||
fileName: download.metadata?.fileName,
|
||||
fileSize: download.metadata?.fileSize,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
public cancelQueuedDownload(downloadId: string): boolean {
|
||||
const index = this.downloadQueue.findIndex((item) => item.downloadId === downloadId);
|
||||
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const canceledDownload = this.downloadQueue.splice(index, 1)[0];
|
||||
canceledDownload.reject(new Error(`Download ${downloadId} was cancelled`));
|
||||
|
||||
console.log(`[DOWNLOAD MANAGER] Cancelled queued download: ${downloadId} (was at position ${index + 1})`);
|
||||
return true;
|
||||
}
|
||||
|
||||
private cleanupStaleDownloads(): void {
|
||||
const now = Date.now();
|
||||
const staleThreshold = 10 * 60 * 1000; // 10 minutes
|
||||
const queueStaleThreshold = 30 * 60 * 1000;
|
||||
|
||||
for (const [downloadId, info] of this.activeDownloads.entries()) {
|
||||
if (now - info.startTime > staleThreshold) {
|
||||
console.warn(`[DOWNLOAD MANAGER] Cleaning up stale active download: ${downloadId}`);
|
||||
this.activeDownloads.delete(downloadId);
|
||||
}
|
||||
}
|
||||
|
||||
const initialQueueLength = this.downloadQueue.length;
|
||||
this.downloadQueue = this.downloadQueue.filter((download) => {
|
||||
if (now - download.queuedAt > queueStaleThreshold) {
|
||||
console.warn(`[DOWNLOAD MANAGER] Cleaning up stale queued download: ${download.downloadId}`);
|
||||
download.reject(new Error(`Download ${download.downloadId} timed out in queue`));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
if (this.downloadQueue.length < initialQueueLength) {
|
||||
console.log(
|
||||
`[DOWNLOAD MANAGER] Cleaned up ${initialQueueLength - this.downloadQueue.length} stale queued downloads`
|
||||
);
|
||||
}
|
||||
|
||||
this.processQueue();
|
||||
}
|
||||
|
||||
public shouldThrottleStream(): boolean {
|
||||
const currentMemoryMB = this.getCurrentMemoryUsageMB();
|
||||
return currentMemoryMB > this.memoryThresholdMB * 0.8;
|
||||
}
|
||||
|
||||
public getThrottleDelay(): number {
|
||||
const currentMemoryMB = this.getCurrentMemoryUsageMB();
|
||||
const thresholdRatio = currentMemoryMB / this.memoryThresholdMB;
|
||||
|
||||
if (thresholdRatio > 0.9) return 200;
|
||||
if (thresholdRatio > 0.8) return 100;
|
||||
return 50;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
if (this.cleanupInterval) {
|
||||
clearInterval(this.cleanupInterval);
|
||||
}
|
||||
|
||||
this.downloadQueue.forEach((download) => {
|
||||
download.reject(new Error("Download manager is shutting down"));
|
||||
});
|
||||
|
||||
this.activeDownloads.clear();
|
||||
this.downloadQueue = [];
|
||||
console.log("[DOWNLOAD MANAGER] Shutdown completed");
|
||||
}
|
||||
|
||||
public clearQueue(): number {
|
||||
const clearedCount = this.downloadQueue.length;
|
||||
|
||||
this.downloadQueue.forEach((download) => {
|
||||
download.reject(new Error("Queue was cleared by administrator"));
|
||||
});
|
||||
|
||||
this.downloadQueue = [];
|
||||
console.log(`[DOWNLOAD MANAGER] Cleared queue: ${clearedCount} downloads cancelled`);
|
||||
return clearedCount;
|
||||
}
|
||||
}
|
378
apps/server/src/utils/mime-types.ts
Normal file
378
apps/server/src/utils/mime-types.ts
Normal file
@@ -0,0 +1,378 @@
|
||||
/**
|
||||
* Utility for detecting MIME types based on file extensions
|
||||
* Fallback to application/octet-stream if extension is unknown
|
||||
*/
|
||||
|
||||
const mimeTypeMap: Record<string, string> = {
|
||||
// Images
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".bmp": "image/bmp",
|
||||
".webp": "image/webp",
|
||||
".svg": "image/svg+xml",
|
||||
".ico": "image/x-icon",
|
||||
".tiff": "image/tiff",
|
||||
".tif": "image/tiff",
|
||||
".avif": "image/avif",
|
||||
".heic": "image/heic",
|
||||
".heif": "image/heif",
|
||||
".jxl": "image/jxl",
|
||||
".psd": "image/vnd.adobe.photoshop",
|
||||
".raw": "image/x-canon-cr2",
|
||||
".cr2": "image/x-canon-cr2",
|
||||
".nef": "image/x-nikon-nef",
|
||||
".arw": "image/x-sony-arw",
|
||||
".dng": "image/x-adobe-dng",
|
||||
".xcf": "image/x-xcf",
|
||||
".pbm": "image/x-portable-bitmap",
|
||||
".pgm": "image/x-portable-graymap",
|
||||
".ppm": "image/x-portable-pixmap",
|
||||
".pnm": "image/x-portable-anymap",
|
||||
|
||||
// Documents
|
||||
".pdf": "application/pdf",
|
||||
".doc": "application/msword",
|
||||
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
||||
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
|
||||
".xls": "application/vnd.ms-excel",
|
||||
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
|
||||
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
|
||||
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
||||
".ppt": "application/vnd.ms-powerpoint",
|
||||
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
||||
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
||||
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
||||
".odt": "application/vnd.oasis.opendocument.text",
|
||||
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
".odp": "application/vnd.oasis.opendocument.presentation",
|
||||
".odg": "application/vnd.oasis.opendocument.graphics",
|
||||
".odf": "application/vnd.oasis.opendocument.formula",
|
||||
".odb": "application/vnd.oasis.opendocument.database",
|
||||
".odc": "application/vnd.oasis.opendocument.chart",
|
||||
".odi": "application/vnd.oasis.opendocument.image",
|
||||
|
||||
// Text and Code
|
||||
".txt": "text/plain",
|
||||
".html": "text/html",
|
||||
".htm": "text/html",
|
||||
".css": "text/css",
|
||||
".js": "application/javascript",
|
||||
".mjs": "application/javascript",
|
||||
".ts": "text/typescript",
|
||||
".tsx": "text/tsx",
|
||||
".jsx": "text/jsx",
|
||||
".json": "application/json",
|
||||
".xml": "application/xml",
|
||||
".csv": "text/csv",
|
||||
".yaml": "text/yaml",
|
||||
".yml": "text/yaml",
|
||||
".toml": "text/plain",
|
||||
".ini": "text/plain",
|
||||
".cfg": "text/plain",
|
||||
".conf": "text/plain",
|
||||
".log": "text/plain",
|
||||
".md": "text/markdown",
|
||||
".markdown": "text/markdown",
|
||||
".rst": "text/x-rst",
|
||||
".tex": "text/x-tex",
|
||||
".latex": "text/x-latex",
|
||||
".rtf": "application/rtf",
|
||||
".ps": "application/postscript",
|
||||
".eps": "application/postscript",
|
||||
|
||||
// Programming Languages
|
||||
".c": "text/x-c",
|
||||
".cc": "text/x-c",
|
||||
".cpp": "text/x-c",
|
||||
".cxx": "text/x-c",
|
||||
".h": "text/x-c",
|
||||
".hpp": "text/x-c",
|
||||
".hxx": "text/x-c",
|
||||
".java": "text/x-java-source",
|
||||
".class": "application/java-vm",
|
||||
".jar": "application/java-archive",
|
||||
".war": "application/java-archive",
|
||||
".py": "text/x-python",
|
||||
".pyw": "text/x-python",
|
||||
".rb": "text/x-ruby",
|
||||
".php": "text/x-php",
|
||||
".pl": "text/x-perl",
|
||||
".pm": "text/x-perl",
|
||||
".sh": "text/x-shellscript",
|
||||
".bash": "text/x-shellscript",
|
||||
".zsh": "text/x-shellscript",
|
||||
".fish": "text/x-shellscript",
|
||||
".bat": "text/x-msdos-batch",
|
||||
".cmd": "text/x-msdos-batch",
|
||||
".ps1": "text/plain",
|
||||
".psm1": "text/plain",
|
||||
".go": "text/x-go",
|
||||
".rs": "text/x-rust",
|
||||
".swift": "text/x-swift",
|
||||
".kt": "text/x-kotlin",
|
||||
".scala": "text/x-scala",
|
||||
".clj": "text/x-clojure",
|
||||
".hs": "text/x-haskell",
|
||||
".elm": "text/x-elm",
|
||||
".dart": "text/x-dart",
|
||||
".r": "text/x-r",
|
||||
".R": "text/x-r",
|
||||
".sql": "text/x-sql",
|
||||
".vb": "text/x-vb",
|
||||
".cs": "text/x-csharp",
|
||||
".fs": "text/x-fsharp",
|
||||
".lua": "text/x-lua",
|
||||
".m": "text/x-objc",
|
||||
".mm": "text/x-objc",
|
||||
|
||||
// Audio
|
||||
".mp3": "audio/mpeg",
|
||||
".mp2": "audio/mpeg",
|
||||
".m4a": "audio/mp4",
|
||||
".m4b": "audio/mp4",
|
||||
".m4p": "audio/mp4",
|
||||
".wav": "audio/wav",
|
||||
".wave": "audio/wav",
|
||||
".aiff": "audio/aiff",
|
||||
".aif": "audio/aiff",
|
||||
".aifc": "audio/aiff",
|
||||
".flac": "audio/flac",
|
||||
".ogg": "audio/ogg",
|
||||
".oga": "audio/ogg",
|
||||
".opus": "audio/opus",
|
||||
".aac": "audio/aac",
|
||||
".wma": "audio/x-ms-wma",
|
||||
".ac3": "audio/ac3",
|
||||
".amr": "audio/amr",
|
||||
".au": "audio/basic",
|
||||
".snd": "audio/basic",
|
||||
".mid": "audio/midi",
|
||||
".midi": "audio/midi",
|
||||
".kar": "audio/midi",
|
||||
".ra": "audio/x-realaudio",
|
||||
".ram": "audio/x-realaudio",
|
||||
".3gp": "audio/3gpp",
|
||||
".3g2": "audio/3gpp2",
|
||||
".spx": "audio/speex",
|
||||
".wv": "audio/x-wavpack",
|
||||
".ape": "audio/x-ape",
|
||||
".mpc": "audio/x-musepack",
|
||||
|
||||
// Video
|
||||
".mp4": "video/mp4",
|
||||
".m4v": "video/mp4",
|
||||
".avi": "video/x-msvideo",
|
||||
".mov": "video/quicktime",
|
||||
".qt": "video/quicktime",
|
||||
".wmv": "video/x-ms-wmv",
|
||||
".asf": "video/x-ms-asf",
|
||||
".flv": "video/x-flv",
|
||||
".f4v": "video/x-f4v",
|
||||
".webm": "video/webm",
|
||||
".mkv": "video/x-matroska",
|
||||
".mka": "audio/x-matroska",
|
||||
".mks": "video/x-matroska",
|
||||
".ogv": "video/ogg",
|
||||
".ogm": "video/ogg",
|
||||
".mxf": "application/mxf",
|
||||
".m2ts": "video/mp2t",
|
||||
".mts": "video/mp2t",
|
||||
".vob": "video/dvd",
|
||||
".mpg": "video/mpeg",
|
||||
".mpeg": "video/mpeg",
|
||||
".m1v": "video/mpeg",
|
||||
".m2v": "video/mpeg",
|
||||
".rm": "application/vnd.rn-realmedia",
|
||||
".rmvb": "application/vnd.rn-realmedia-vbr",
|
||||
".divx": "video/divx",
|
||||
".xvid": "video/x-xvid",
|
||||
|
||||
// Archives and Compression
|
||||
".zip": "application/zip",
|
||||
".rar": "application/vnd.rar",
|
||||
".7z": "application/x-7z-compressed",
|
||||
".tar": "application/x-tar",
|
||||
".tar.gz": "application/gzip",
|
||||
".tgz": "application/gzip",
|
||||
".tar.bz2": "application/x-bzip2",
|
||||
".tbz2": "application/x-bzip2",
|
||||
".tar.xz": "application/x-xz",
|
||||
".txz": "application/x-xz",
|
||||
".tar.lz": "application/x-lzip",
|
||||
".tar.Z": "application/x-compress",
|
||||
".Z": "application/x-compress",
|
||||
".gz": "application/gzip",
|
||||
".bz2": "application/x-bzip2",
|
||||
".xz": "application/x-xz",
|
||||
".lz": "application/x-lzip",
|
||||
".lzma": "application/x-lzma",
|
||||
".lzo": "application/x-lzop",
|
||||
".arj": "application/x-arj",
|
||||
".ace": "application/x-ace-compressed",
|
||||
".cab": "application/vnd.ms-cab-compressed",
|
||||
".iso": "application/x-iso9660-image",
|
||||
".dmg": "application/x-apple-diskimage",
|
||||
".img": "application/x-img",
|
||||
".bin": "application/octet-stream",
|
||||
".cue": "application/x-cue",
|
||||
".nrg": "application/x-nrg",
|
||||
".mdf": "application/x-mdf",
|
||||
".toast": "application/x-toast",
|
||||
|
||||
// Executables and System
|
||||
".exe": "application/vnd.microsoft.portable-executable",
|
||||
".dll": "application/vnd.microsoft.portable-executable",
|
||||
".msi": "application/x-msdownload",
|
||||
".msp": "application/x-msdownload",
|
||||
".deb": "application/vnd.debian.binary-package",
|
||||
".rpm": "application/x-rpm",
|
||||
".pkg": "application/x-newton-compatible-pkg",
|
||||
".apk": "application/vnd.android.package-archive",
|
||||
".ipa": "application/octet-stream",
|
||||
".app": "application/octet-stream",
|
||||
".snap": "application/x-snap",
|
||||
".flatpak": "application/vnd.flatpak",
|
||||
".appimage": "application/x-appimage",
|
||||
|
||||
// Adobe and Design
|
||||
".ai": "application/illustrator",
|
||||
".indd": "application/x-indesign",
|
||||
".idml": "application/vnd.adobe.indesign-idml-package",
|
||||
".sketch": "application/x-sketch",
|
||||
".fig": "application/x-figma",
|
||||
".xd": "application/vnd.adobe.xd",
|
||||
|
||||
// Fonts
|
||||
".ttf": "font/ttf",
|
||||
".otf": "font/otf",
|
||||
".woff": "font/woff",
|
||||
".woff2": "font/woff2",
|
||||
".eot": "application/vnd.ms-fontobject",
|
||||
".fon": "application/x-font-bdf",
|
||||
".bdf": "application/x-font-bdf",
|
||||
".pcf": "application/x-font-pcf",
|
||||
".pfb": "application/x-font-type1",
|
||||
".pfm": "application/x-font-type1",
|
||||
".afm": "application/x-font-afm",
|
||||
|
||||
// E-books
|
||||
".epub": "application/epub+zip",
|
||||
".mobi": "application/x-mobipocket-ebook",
|
||||
".azw": "application/vnd.amazon.ebook",
|
||||
".azw3": "application/vnd.amazon.ebook",
|
||||
".fb2": "application/x-fictionbook+xml",
|
||||
".lit": "application/x-ms-reader",
|
||||
".pdb": "application/vnd.palm",
|
||||
".prc": "application/vnd.palm",
|
||||
".tcr": "application/x-psion3-s",
|
||||
|
||||
// CAD and 3D
|
||||
".dwg": "image/vnd.dwg",
|
||||
".dxf": "image/vnd.dxf",
|
||||
".step": "application/step",
|
||||
".stp": "application/step",
|
||||
".iges": "application/iges",
|
||||
".igs": "application/iges",
|
||||
".stl": "application/sla",
|
||||
".obj": "application/x-tgif",
|
||||
".3ds": "application/x-3ds",
|
||||
".dae": "model/vnd.collada+xml",
|
||||
".ply": "application/ply",
|
||||
".x3d": "model/x3d+xml",
|
||||
|
||||
// Database
|
||||
".db": "application/x-sqlite3",
|
||||
".sqlite": "application/x-sqlite3",
|
||||
".sqlite3": "application/x-sqlite3",
|
||||
".mdb": "application/x-msaccess",
|
||||
".accdb": "application/x-msaccess",
|
||||
|
||||
// Virtual Machine and Disk Images
|
||||
".vmdk": "application/x-vmdk",
|
||||
".vdi": "application/x-virtualbox-vdi",
|
||||
".vhd": "application/x-virtualbox-vhd",
|
||||
".vhdx": "application/x-virtualbox-vhdx",
|
||||
".ova": "application/x-virtualbox-ova",
|
||||
".ovf": "application/x-virtualbox-ovf",
|
||||
".qcow2": "application/x-qemu-disk",
|
||||
|
||||
// Scientific and Math
|
||||
".mat": "application/x-matlab-data",
|
||||
".nc": "application/x-netcdf",
|
||||
".cdf": "application/x-netcdf",
|
||||
".hdf": "application/x-hdf",
|
||||
".h5": "application/x-hdf5",
|
||||
|
||||
// Misc Application Formats
|
||||
".torrent": "application/x-bittorrent",
|
||||
".rss": "application/rss+xml",
|
||||
".atom": "application/atom+xml",
|
||||
".gpx": "application/gpx+xml",
|
||||
".kml": "application/vnd.google-earth.kml+xml",
|
||||
".kmz": "application/vnd.google-earth.kmz",
|
||||
".ics": "text/calendar",
|
||||
".vcs": "text/x-vcalendar",
|
||||
".vcf": "text/x-vcard",
|
||||
".p7s": "application/pkcs7-signature",
|
||||
".p7m": "application/pkcs7-mime",
|
||||
".p12": "application/x-pkcs12",
|
||||
".pfx": "application/x-pkcs12",
|
||||
".cer": "application/x-x509-ca-cert",
|
||||
".crt": "application/x-x509-ca-cert",
|
||||
".pem": "application/x-pem-file",
|
||||
".key": "application/x-pem-file",
|
||||
};
|
||||
|
||||
/**
|
||||
* Get MIME type from file extension
|
||||
* @param filename - The filename or extension (with or without leading dot)
|
||||
* @returns MIME type string, defaults to 'application/octet-stream' if unknown
|
||||
*/
|
||||
export function getMimeType(filename: string): string {
|
||||
if (!filename) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
let extension: string;
|
||||
|
||||
if (filename.startsWith(".")) {
|
||||
extension = filename.toLowerCase();
|
||||
} else if (!filename.includes(".")) {
|
||||
extension = "." + filename.toLowerCase();
|
||||
} else {
|
||||
const lastDotIndex = filename.lastIndexOf(".");
|
||||
if (lastDotIndex === -1) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
extension = filename.substring(lastDotIndex).toLowerCase();
|
||||
}
|
||||
|
||||
return mimeTypeMap[extension] || "application/octet-stream";
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a MIME type represents an image
|
||||
* @param mimeType - The MIME type to check
|
||||
* @returns true if the MIME type is an image type
|
||||
*/
|
||||
export function isImageMimeType(mimeType: string): boolean {
|
||||
return mimeType.startsWith("image/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get appropriate Content-Type header value for a file
|
||||
* @param filename - The filename to detect type for
|
||||
* @returns Content-Type header value
|
||||
*/
|
||||
export function getContentType(filename: string): string {
|
||||
return getMimeType(filename);
|
||||
}
|
@@ -1,2 +1,5 @@
|
||||
API_BASE_URL=http:localhost:3333
|
||||
NEXT_PUBLIC_DEFAULT_LANGUAGE=en-US
|
||||
NEXT_PUBLIC_DEFAULT_LANGUAGE=en-US
|
||||
|
||||
# Configuration options
|
||||
NEXT_PUBLIC_UPLOAD_CHUNK_SIZE_MB=
|
@@ -144,7 +144,13 @@
|
||||
"update": "تحديث",
|
||||
"click": "انقر على",
|
||||
"creating": "جاري الإنشاء...",
|
||||
"loadingSimple": "جاري التحميل..."
|
||||
"loadingSimple": "جاري التحميل...",
|
||||
"create": "إنشاء",
|
||||
"deleting": "جاري الحذف...",
|
||||
"move": "نقل",
|
||||
"rename": "إعادة تسمية",
|
||||
"search": "بحث",
|
||||
"share": "مشاركة"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "إنشاء مشاركة",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "إنشاء مشاركة",
|
||||
"success": "تم إنشاء المشاركة بنجاح",
|
||||
"error": "فشل في إنشاء المشاركة",
|
||||
"namePlaceholder": "أدخل اسمًا لمشاركتك"
|
||||
"namePlaceholder": "أدخل اسمًا لمشاركتك",
|
||||
"nextSelectFiles": "التالي: اختيار الملفات",
|
||||
"searchLabel": "بحث",
|
||||
"tabs": {
|
||||
"shareDetails": "تفاصيل المشاركة",
|
||||
"selectFiles": "اختيار الملفات"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "التخصيص",
|
||||
"colors": {
|
||||
"title": "ألوان السمة",
|
||||
"description": "اختر لون السمة الرئيسي المفضل لديك",
|
||||
"presets": "الألوان المتاحة",
|
||||
"presetsDescription": "اختر من بين السمات اللونية المتاحة",
|
||||
"reset": "إعادة التعيين إلى الافتراضي"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "الخطوط",
|
||||
"description": "اختر عائلة الخط المفضلة لديك",
|
||||
"available": "الخطوط المتاحة",
|
||||
"availableDescription": "اختر من بين عائلات الخطوط المتاحة",
|
||||
"reset": "إعادة التعيين إلى الافتراضي"
|
||||
},
|
||||
"radius": {
|
||||
"title": "حواف الإطار",
|
||||
"description": "تخصيص استدارة عناصر الواجهة",
|
||||
"available": "خيارات الاستدارة",
|
||||
"availableDescription": "اختر كيف يجب أن تظهر الزوايا المستديرة",
|
||||
"reset": "إعادة التعيين إلى الافتراضي"
|
||||
},
|
||||
"background": {
|
||||
"title": "ألوان الخلفية",
|
||||
"description": "تخصيص ألوان الخلفية للوضعين الفاتح والداكن",
|
||||
"lightMode": "الوضع الفاتح",
|
||||
"darkMode": "الوضع الداكن",
|
||||
"availableDescription": "اختر ألوان الخلفية لكل من السمات الفاتحة والداكنة",
|
||||
"reset": "إعادة التعيين إلى الافتراضي"
|
||||
},
|
||||
"theme": {
|
||||
"title": "وضع السمة",
|
||||
"description": "اختر بين السمة الفاتحة أو الداكنة أو سمة النظام",
|
||||
"selectTheme": "تفضيلات السمة",
|
||||
"availableDescription": "حدد وضع السمة المفضل لديك",
|
||||
"reset": "إعادة التعيين إلى النظام"
|
||||
},
|
||||
"pageTitle": "التخصيص"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "فشل في تحميل بيانات لوحة التحكم",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "الملفات المراد حذفها",
|
||||
"sharesToDelete": "المشاركات التي سيتم حذفها"
|
||||
"sharesToDelete": "المشاركات التي سيتم حذفها",
|
||||
"foldersToDelete": "المجلدات المراد حذفها",
|
||||
"itemsToDelete": "العناصر المراد حذفها"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "تم إضافة التنزيل إلى قائمة الانتظار: {fileName}",
|
||||
"queuedDescription": "سيبدأ التنزيل تلقائياً عندما يتوفر موقع",
|
||||
"queuePosition": "التنزيل في قائمة الانتظار في الموقع {position}: {fileName}",
|
||||
"estimatedWait": "وقت الانتظار المقدر: {time}",
|
||||
"queueFull": "قائمة انتظار التنزيل ممتلئة",
|
||||
"queueFullDescription": "يرجى المحاولة مرة أخرى بعد بضع دقائق عندما تتوفر مساحة في قائمة الانتظار",
|
||||
"cancelSuccess": "تم إلغاء التنزيل بنجاح",
|
||||
"cancelError": "فشل إلغاء التنزيل: {error}",
|
||||
"status": {
|
||||
"pending": "جارٍ التحضير...",
|
||||
"queued": "في قائمة الانتظار",
|
||||
"downloading": "جارٍ التنزيل",
|
||||
"completed": "مكتمل",
|
||||
"failed": "فشل"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds} ثانية",
|
||||
"minutes": "{minutes} دقيقة",
|
||||
"hoursMinutes": "{hours} ساعة {minutes} دقيقة"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "التنزيلات",
|
||||
"downloads": "قائمة التنزيل",
|
||||
"active": "نشط",
|
||||
"queued": "في قائمة الانتظار",
|
||||
"position": "الموقع {position}",
|
||||
"estimatedWait": "الانتظار: {time}",
|
||||
"unknownFile": "ملف غير معروف",
|
||||
"noDownloads": "لا توجد تنزيلات قيد التقدم",
|
||||
"refresh": "تحديث قائمة الانتظار"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "لم يتم رفع أي ملفات بعد",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "أدخل وصف الملف",
|
||||
"deleteFile": "حذف الملف",
|
||||
"deleteConfirmation": "هل أنت متأكد أنك تريد حذف ؟",
|
||||
"deleteWarning": "هذا الإجراء لا يمكن التراجع عنه."
|
||||
"deleteWarning": "هذا الإجراء لا يمكن التراجع عنه.",
|
||||
"addDescriptionPlaceholder": "إضافة وصف..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "فشل في تنزيل الملف",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "معاينة الملف",
|
||||
"addToShare": "إضافة إلى المشاركة",
|
||||
"removeFromShare": "إزالة من المشاركة",
|
||||
"saveChanges": "حفظ التغييرات"
|
||||
"saveChanges": "حفظ التغييرات",
|
||||
"editFolder": "تحرير المجلد"
|
||||
},
|
||||
"files": {
|
||||
"title": "جميع الملفات",
|
||||
@@ -271,7 +360,20 @@
|
||||
"table": "جدول",
|
||||
"grid": "شبكة"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {لا توجد ملفات} =1 {ملف واحد} other {# ملفات}}"
|
||||
"totalFiles": "{count, plural, =0 {لا توجد ملفات} =1 {ملف واحد} other {# ملفات}}",
|
||||
"bulkDeleteConfirmation": "هل أنت متأكد من رغبتك في حذف {count, plural, =1 {ملف واحد} other {# ملفات}}؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"bulkDeleteTitle": "حذف الملفات المحددة",
|
||||
"actions": {
|
||||
"open": "فتح",
|
||||
"rename": "إعادة تسمية",
|
||||
"delete": "حذف"
|
||||
},
|
||||
"empty": {
|
||||
"title": "لا توجد ملفات أو مجلدات بعد",
|
||||
"description": "ارفع ملفك الأول أو أنشئ مجلدًا للبدء"
|
||||
},
|
||||
"files": "ملفات",
|
||||
"folders": "مجلدات"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "جدول الملفات",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "حذف المحدد"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "تحرير المجلد",
|
||||
"folderName": "اسم المجلد",
|
||||
"folderNamePlaceholder": "أدخل اسم المجلد",
|
||||
"folderDescription": "الوصف",
|
||||
"folderDescriptionPlaceholder": "أدخل وصف المجلد (اختياري)",
|
||||
"createFolder": "إنشاء مجلد جديد",
|
||||
"renameFolder": "إعادة تسمية المجلد",
|
||||
"moveFolder": "نقل المجلد",
|
||||
"shareFolder": "مشاركة المجلد",
|
||||
"deleteFolder": "حذف المجلد",
|
||||
"moveTo": "نقل إلى",
|
||||
"selectDestination": "اختر مجلد الوجهة",
|
||||
"rootFolder": "الجذر",
|
||||
"folderCreated": "تم إنشاء المجلد بنجاح",
|
||||
"folderRenamed": "تم إعادة تسمية المجلد بنجاح",
|
||||
"folderMoved": "تم نقل المجلد بنجاح",
|
||||
"folderDeleted": "تم حذف المجلد بنجاح",
|
||||
"folderShared": "تم مشاركة المجلد بنجاح",
|
||||
"createFolderError": "خطأ في إنشاء المجلد",
|
||||
"renameFolderError": "خطأ في إعادة تسمية المجلد",
|
||||
"moveFolderError": "خطأ في نقل المجلد",
|
||||
"deleteFolderError": "خطأ في حذف المجلد",
|
||||
"shareFolderError": "خطأ في مشاركة المجلد",
|
||||
"deleteConfirmation": "هل أنت متأكد من أنك تريد حذف هذا المجلد؟",
|
||||
"deleteWarning": "لا يمكن التراجع عن هذا الإجراء."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "مدعوم من",
|
||||
"kyanHomepage": "الصفحة الرئيسية لـ Kyantech"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "فشل في حذف الشعار"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "العناصر المراد نقلها:",
|
||||
"movingTo": "النقل إلى:",
|
||||
"title": "نقل {count, plural, =1 {عنصر} other {عناصر}}",
|
||||
"description": "نقل {count, plural, =1 {عنصر} other {عناصر}} إلى موقع جديد",
|
||||
"success": "تم نقل {count} {count, plural, =1 {عنصر} other {عناصر}} بنجاح"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "شعار التطبيق",
|
||||
"profileMenu": "قائمة الملف الشخصي",
|
||||
"profile": "الملف الشخصي",
|
||||
"settings": "الإعدادات",
|
||||
"usersManagement": "إدارة المستخدمين",
|
||||
"logout": "تسجيل الخروج"
|
||||
"logout": "تسجيل الخروج",
|
||||
"customization": "التخصيص"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "لوحة التحكم"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "تم تمكين إشعارات التنزيل",
|
||||
"permissionDenied": "تم تعطيل إشعارات التنزيل",
|
||||
"downloadComplete": {
|
||||
"title": "اكتمل التنزيل",
|
||||
"body": "اكتمل تنزيل {fileName}"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "فشل التنزيل",
|
||||
"body": "فشل تنزيل {fileName}: {error}",
|
||||
"unknownError": "خطأ غير معروف"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "بدء التنزيل",
|
||||
"body": "يتم الآن تنزيل {fileName}{position}",
|
||||
"position": " (كان #{position} في قائمة الانتظار)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "تغيير كلمة المرور",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "انتهت مهلة عملية النسخ. يرجى المحاولة مرة أخرى باستخدام ملف أصغر أو التحقق من اتصالك.",
|
||||
"failed": "فشلت عملية النسخ. يرجى المحاولة مرة أخرى.",
|
||||
"aborted": "تم إلغاء عملية النسخ بسبب انتهاء المهلة."
|
||||
}
|
||||
},
|
||||
"invalidDate": "تاريخ غير صحيح"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1012,7 +1168,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "ابحث عن الملفات...",
|
||||
"results": "تم العثور على {filtered} من {total} ملف"
|
||||
"results": "تم العثور على {filtered} من {total} ملف",
|
||||
"placeholderFolders": "البحث في المجلدات...",
|
||||
"noResults": "لم يتم العثور على نتائج لـ \"{query}\"",
|
||||
"placeholderFiles": "البحث في الملفات..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1225,7 +1384,18 @@
|
||||
"editSuccess": "تم تحديث المشاركة بنجاح",
|
||||
"editError": "فشل في تحديث المشاركة",
|
||||
"bulkDeleteConfirmation": "هل أنت متأكد من أنك تريد حذف {count, plural, =1 {مشاركة واحدة} other {# مشاركات}} محددة؟ لا يمكن التراجع عن هذا الإجراء.",
|
||||
"bulkDeleteTitle": "حذف المشاركات المحددة"
|
||||
"bulkDeleteTitle": "حذف المشاركات المحددة",
|
||||
"addDescriptionPlaceholder": "إضافة وصف...",
|
||||
"aliasLabel": "اسم مستعار للرابط",
|
||||
"aliasPlaceholder": "أدخل اسمًا مستعارًا مخصصًا",
|
||||
"copyLink": "نسخ الرابط",
|
||||
"fileTitle": "مشاركة ملف",
|
||||
"folderTitle": "مشاركة مجلد",
|
||||
"generateLink": "إنشاء رابط",
|
||||
"linkDescriptionFile": "إنشاء رابط مخصص لمشاركة الملف",
|
||||
"linkDescriptionFolder": "إنشاء رابط مخصص لمشاركة المجلد",
|
||||
"linkReady": "رابط المشاركة جاهز:",
|
||||
"linkTitle": "إنشاء رابط"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "تفاصيل المشاركة",
|
||||
@@ -1352,7 +1522,8 @@
|
||||
"files": "ملفات",
|
||||
"totalSize": "الحجم الإجمالي",
|
||||
"creating": "جاري الإنشاء...",
|
||||
"create": "إنشاء مشاركة"
|
||||
"create": "إنشاء مشاركة",
|
||||
"itemsToShare": "العناصر للمشاركة ({count} {count, plural, =1 {عنصر} other {عناصر}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "تكوين حماية كلمة المرور وخيارات الأمان لهذه المشاركة",
|
||||
@@ -1457,7 +1628,8 @@
|
||||
"download": "تنزيل محدد"
|
||||
},
|
||||
"selectAll": "تحديد الكل",
|
||||
"selectShare": "تحديد المشاركة {shareName}"
|
||||
"selectShare": "تحديد المشاركة {shareName}",
|
||||
"folderCount": "مجلدات"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "استخدام التخزين",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "Aktualisieren",
|
||||
"click": "Klicken Sie auf",
|
||||
"creating": "Erstellen...",
|
||||
"loadingSimple": "Lade..."
|
||||
"loadingSimple": "Lade...",
|
||||
"create": "Erstellen",
|
||||
"deleting": "Lösche...",
|
||||
"move": "Verschieben",
|
||||
"rename": "Umbenennen",
|
||||
"search": "Suchen",
|
||||
"share": "Teilen"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Freigabe Erstellen",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "Freigabe Erstellen",
|
||||
"success": "Freigabe erfolgreich erstellt",
|
||||
"error": "Fehler beim Erstellen der Freigabe",
|
||||
"namePlaceholder": "Geben Sie einen Namen für Ihre Freigabe ein"
|
||||
"namePlaceholder": "Geben Sie einen Namen für Ihre Freigabe ein",
|
||||
"nextSelectFiles": "Weiter: Dateien auswählen",
|
||||
"searchLabel": "Suchen",
|
||||
"tabs": {
|
||||
"shareDetails": "Freigabe-Details",
|
||||
"selectFiles": "Dateien auswählen"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Anpassung",
|
||||
"colors": {
|
||||
"title": "Farbthema",
|
||||
"description": "Wählen Sie Ihre bevorzugte Primärfarbe für das Theme",
|
||||
"presets": "Verfügbare Farben",
|
||||
"presetsDescription": "Wählen Sie aus verfügbaren Farbthemen",
|
||||
"reset": "Auf Standard zurücksetzen"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Typografie",
|
||||
"description": "Wählen Sie Ihre bevorzugte Schriftart",
|
||||
"available": "Verfügbare Schriftarten",
|
||||
"availableDescription": "Wählen Sie aus verfügbaren Schriftfamilien",
|
||||
"reset": "Auf Standard zurücksetzen"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Rahmenradius",
|
||||
"description": "Passen Sie die Rundung der Oberflächenelemente an",
|
||||
"available": "Rundungsoptionen",
|
||||
"availableDescription": "Wählen Sie, wie abgerundet die Ecken erscheinen sollen",
|
||||
"reset": "Auf Standard zurücksetzen"
|
||||
},
|
||||
"background": {
|
||||
"title": "Hintergrundfarben",
|
||||
"description": "Passen Sie Hintergrundfarben für Hell- und Dunkelmodus an",
|
||||
"lightMode": "Hellmodus",
|
||||
"darkMode": "Dunkelmodus",
|
||||
"availableDescription": "Wählen Sie Hintergrundfarben für helle und dunkle Themes",
|
||||
"reset": "Auf Standard zurücksetzen"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Themenmodus",
|
||||
"description": "Wählen Sie zwischen hellem, dunklem oder Systemmodus",
|
||||
"selectTheme": "Theme-Einstellung",
|
||||
"availableDescription": "Wählen Sie Ihren bevorzugten Themenmodus",
|
||||
"reset": "Auf System zurücksetzen"
|
||||
},
|
||||
"pageTitle": "Anpassung"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Fehler beim Laden der Dashboard-Daten",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Zu löschende Dateien",
|
||||
"sharesToDelete": "Freigaben, die gelöscht werden"
|
||||
"sharesToDelete": "Freigaben, die gelöscht werden",
|
||||
"foldersToDelete": "Zu löschende Ordner",
|
||||
"itemsToDelete": "Zu löschende Elemente"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Download in Warteschlange: {fileName}",
|
||||
"queuedDescription": "Ihr Download startet automatisch, sobald ein Platz frei wird",
|
||||
"queuePosition": "Download an Position {position} in der Warteschlange: {fileName}",
|
||||
"estimatedWait": "Geschätzte Wartezeit: {time}",
|
||||
"queueFull": "Download-Warteschlange ist voll",
|
||||
"queueFullDescription": "Bitte versuchen Sie es in einigen Minuten erneut, wenn die Warteschlange Platz hat",
|
||||
"cancelSuccess": "Download erfolgreich abgebrochen",
|
||||
"cancelError": "Download konnte nicht abgebrochen werden: {error}",
|
||||
"status": {
|
||||
"pending": "Wird vorbereitet...",
|
||||
"queued": "In Warteschlange",
|
||||
"downloading": "Wird heruntergeladen",
|
||||
"completed": "Abgeschlossen",
|
||||
"failed": "Fehlgeschlagen"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}s",
|
||||
"minutes": "{minutes}m",
|
||||
"hoursMinutes": "{hours}h {minutes}m"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Downloads",
|
||||
"downloads": "Download-Warteschlange",
|
||||
"active": "Aktiv",
|
||||
"queued": "In Warteschlange",
|
||||
"position": "Position {position}",
|
||||
"estimatedWait": "Wartezeit: {time}",
|
||||
"unknownFile": "Unbekannte Datei",
|
||||
"noDownloads": "Keine laufenden Downloads",
|
||||
"refresh": "Warteschlange aktualisieren"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Noch keine Dateien hochgeladen",
|
||||
@@ -201,6 +288,7 @@
|
||||
"extension": "Erweiterung",
|
||||
"descriptionLabel": "Beschreibung",
|
||||
"descriptionPlaceholder": "Dateibeschreibung eingeben",
|
||||
"addDescriptionPlaceholder": "Beschreibung hinzufügen...",
|
||||
"deleteFile": "Datei löschen",
|
||||
"deleteConfirmation": "Sind Sie sicher, dass Sie löschen möchten?",
|
||||
"deleteWarning": "Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Datei-Vorschau",
|
||||
"addToShare": "Zur Freigabe hinzufügen",
|
||||
"removeFromShare": "Aus Freigabe entfernen",
|
||||
"saveChanges": "Änderungen Speichern"
|
||||
"saveChanges": "Änderungen Speichern",
|
||||
"editFolder": "Ordner bearbeiten"
|
||||
},
|
||||
"files": {
|
||||
"title": "Alle Dateien",
|
||||
@@ -267,11 +356,24 @@
|
||||
"bulkDownloadFileError": "Fehler beim Herunterladen der Datei {fileName}",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 Datei erfolgreich gelöscht} other {# Dateien erfolgreich gelöscht}}",
|
||||
"bulkDeleteError": "Fehler beim Löschen der ausgewählten Dateien",
|
||||
"bulkDeleteTitle": "Ausgewählte Dateien Löschen",
|
||||
"bulkDeleteConfirmation": "Sind Sie sicher, dass Sie {count, plural, =1 {1 Datei} other {# Dateien}} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"viewMode": {
|
||||
"table": "Tabelle",
|
||||
"grid": "Raster"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {Keine Dateien} =1 {1 Datei} other {# Dateien}}"
|
||||
"totalFiles": "{count, plural, =0 {Keine Dateien} =1 {1 Datei} other {# Dateien}}",
|
||||
"actions": {
|
||||
"open": "Öffnen",
|
||||
"rename": "Umbenennen",
|
||||
"delete": "Löschen"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Noch keine Dateien oder Ordner",
|
||||
"description": "Laden Sie Ihre erste Datei hoch oder erstellen Sie einen Ordner, um zu beginnen"
|
||||
},
|
||||
"files": "Dateien",
|
||||
"folders": "Ordner"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Dateien-Tabelle",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Ausgewählte Löschen"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Ordner bearbeiten",
|
||||
"folderName": "Ordnername",
|
||||
"folderNamePlaceholder": "Ordnername eingeben",
|
||||
"folderDescription": "Beschreibung",
|
||||
"folderDescriptionPlaceholder": "Ordnerbeschreibung eingeben (optional)",
|
||||
"createFolder": "Neuen Ordner erstellen",
|
||||
"renameFolder": "Ordner umbenennen",
|
||||
"moveFolder": "Ordner verschieben",
|
||||
"shareFolder": "Ordner teilen",
|
||||
"deleteFolder": "Ordner löschen",
|
||||
"moveTo": "Verschieben nach",
|
||||
"selectDestination": "Zielordner auswählen",
|
||||
"rootFolder": "Stammordner",
|
||||
"folderCreated": "Ordner erfolgreich erstellt",
|
||||
"folderRenamed": "Ordner erfolgreich umbenannt",
|
||||
"folderMoved": "Ordner erfolgreich verschoben",
|
||||
"folderDeleted": "Ordner erfolgreich gelöscht",
|
||||
"folderShared": "Ordner erfolgreich geteilt",
|
||||
"createFolderError": "Fehler beim Erstellen des Ordners",
|
||||
"renameFolderError": "Fehler beim Umbenennen des Ordners",
|
||||
"moveFolderError": "Fehler beim Verschieben des Ordners",
|
||||
"deleteFolderError": "Fehler beim Löschen des Ordners",
|
||||
"shareFolderError": "Fehler beim Teilen des Ordners",
|
||||
"deleteConfirmation": "Sind Sie sicher, dass Sie diesen Ordner löschen möchten?",
|
||||
"deleteWarning": "Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Angetrieben von",
|
||||
"kyanHomepage": "Kyantech Homepage"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Fehler beim Entfernen des Logos"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Zu verschiebende Elemente:",
|
||||
"movingTo": "Verschieben nach:",
|
||||
"title": "{count, plural, =1 {Element} other {Elemente}} verschieben",
|
||||
"description": "{count, plural, =1 {Element} other {Elemente}} an einen neuen Ort verschieben",
|
||||
"success": "Erfolgreich {count} {count, plural, =1 {Element} other {Elemente}} verschoben"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Anwendungslogo",
|
||||
"profileMenu": "Profilmenü",
|
||||
"profile": "Profil",
|
||||
"settings": "Einstellungen",
|
||||
"usersManagement": "Benutzerverwaltung",
|
||||
"logout": "Abmelden"
|
||||
"logout": "Abmelden",
|
||||
"customization": "Anpassung"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Übersicht"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Download-Benachrichtigungen aktiviert",
|
||||
"permissionDenied": "Download-Benachrichtigungen deaktiviert",
|
||||
"downloadComplete": {
|
||||
"title": "Download abgeschlossen",
|
||||
"body": "{fileName} wurde erfolgreich heruntergeladen"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Download fehlgeschlagen",
|
||||
"body": "Fehler beim Herunterladen von {fileName}: {error}",
|
||||
"unknownError": "Unbekannter Fehler"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Download startet",
|
||||
"body": "{fileName} wird jetzt heruntergeladen{position}",
|
||||
"position": " (war #{position} in der Warteschlange)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Passwort ändern",
|
||||
@@ -750,6 +905,7 @@
|
||||
"noFiles": "Noch keine Dateien empfangen",
|
||||
"noFilesDescription": "Über diesen Link gesendete Dateien erscheinen hier",
|
||||
"fileCount": "{count, plural, =0 {Keine Dateien} =1 {1 Datei} other {# Dateien}}",
|
||||
"invalidDate": "Ungültiges Datum",
|
||||
"totalSize": "Gesamtgröße: {size}",
|
||||
"columns": {
|
||||
"file": "Datei",
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Dateien suchen...",
|
||||
"results": "Gefunden {filtered} von {total} Dateien"
|
||||
"results": "Gefunden {filtered} von {total} Dateien",
|
||||
"placeholderFolders": "Ordner durchsuchen...",
|
||||
"noResults": "Keine Ergebnisse für \"{query}\" gefunden",
|
||||
"placeholderFiles": "Dateien suchen..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1205,6 +1364,7 @@
|
||||
"shareActions": {
|
||||
"deleteTitle": "Freigabe Löschen",
|
||||
"deleteConfirmation": "Sind Sie sicher, dass Sie diese Freigabe löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"addDescriptionPlaceholder": "Beschreibung hinzufügen...",
|
||||
"editTitle": "Freigabe Bearbeiten",
|
||||
"nameLabel": "Freigabe-Name",
|
||||
"descriptionLabel": "Beschreibung",
|
||||
@@ -1223,7 +1383,17 @@
|
||||
"editSuccess": "Freigabe erfolgreich aktualisiert",
|
||||
"editError": "Fehler beim Aktualisieren der Freigabe",
|
||||
"bulkDeleteConfirmation": "Sind Sie sicher, dass Sie {count, plural, =1 {1 Freigabe} other {# Freigaben}} löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
"bulkDeleteTitle": "Ausgewählte Freigaben löschen"
|
||||
"bulkDeleteTitle": "Ausgewählte Freigaben löschen",
|
||||
"aliasLabel": "Link-Alias",
|
||||
"aliasPlaceholder": "Benutzerdefinierten Alias eingeben",
|
||||
"copyLink": "Link kopieren",
|
||||
"fileTitle": "Datei teilen",
|
||||
"folderTitle": "Ordner teilen",
|
||||
"generateLink": "Link generieren",
|
||||
"linkDescriptionFile": "Erstellen Sie einen benutzerdefinierten Link zum Teilen der Datei",
|
||||
"linkDescriptionFolder": "Erstellen Sie einen benutzerdefinierten Link zum Teilen des Ordners",
|
||||
"linkReady": "Ihr Freigabe-Link ist bereit:",
|
||||
"linkTitle": "Link generieren"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Freigabe-Details",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "Dateien",
|
||||
"totalSize": "Gesamtgröße",
|
||||
"creating": "Erstellen...",
|
||||
"create": "Freigabe Erstellen"
|
||||
"create": "Freigabe Erstellen",
|
||||
"itemsToShare": "Zu teilende Elemente ({count} {count, plural, =1 {Element} other {Elemente}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "Passwortschutz und Sicherheitsoptionen für diese Freigabe konfigurieren",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "Download ausgewählt"
|
||||
},
|
||||
"selectAll": "Alle auswählen",
|
||||
"selectShare": "Freigabe {shareName} auswählen"
|
||||
"selectShare": "Freigabe {shareName} auswählen",
|
||||
"folderCount": "Ordner"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "Speichernutzung",
|
||||
|
@@ -136,6 +136,7 @@
|
||||
"update": "Update",
|
||||
"updating": "Updating...",
|
||||
"delete": "Delete",
|
||||
"deleting": "Deleting...",
|
||||
"close": "Close",
|
||||
"download": "Download",
|
||||
"unexpectedError": "An unexpected error occurred. Please try again.",
|
||||
@@ -144,7 +145,12 @@
|
||||
"dashboard": "Dashboard",
|
||||
"back": "Back",
|
||||
"click": "Click to",
|
||||
"creating": "Creating..."
|
||||
"creating": "Creating...",
|
||||
"create": "Create",
|
||||
"rename": "Rename",
|
||||
"move": "Move",
|
||||
"share": "Share",
|
||||
"search": "Search"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Create Share",
|
||||
@@ -160,7 +166,53 @@
|
||||
"passwordLabel": "Password",
|
||||
"create": "Create Share",
|
||||
"success": "Share created successfully",
|
||||
"error": "Failed to create share"
|
||||
"error": "Failed to create share",
|
||||
"tabs": {
|
||||
"shareDetails": "Share Details",
|
||||
"selectFiles": "Select Files"
|
||||
},
|
||||
"nextSelectFiles": "Next: Select Files",
|
||||
"searchLabel": "Search"
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Customization",
|
||||
"colors": {
|
||||
"title": "Theme Colors",
|
||||
"description": "Choose your preferred primary color theme",
|
||||
"presets": "Available Colors",
|
||||
"presetsDescription": "Select from available color themes",
|
||||
"reset": "Reset to Default"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Typography",
|
||||
"description": "Choose your preferred font family",
|
||||
"available": "Available Fonts",
|
||||
"availableDescription": "Select from available font families",
|
||||
"reset": "Reset to Default"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Border Radius",
|
||||
"description": "Customize the roundness of interface elements",
|
||||
"available": "Roundness Options",
|
||||
"availableDescription": "Choose how rounded corners should appear",
|
||||
"reset": "Reset to Default"
|
||||
},
|
||||
"background": {
|
||||
"title": "Background Colors",
|
||||
"description": "Customize background colors for light and dark modes",
|
||||
"lightMode": "Light Mode",
|
||||
"darkMode": "Dark Mode",
|
||||
"availableDescription": "Choose background colors for both light and dark themes",
|
||||
"reset": "Reset to Default"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme Mode",
|
||||
"description": "Choose between light, dark, or system theme",
|
||||
"selectTheme": "Theme Preference",
|
||||
"availableDescription": "Select your preferred theme mode",
|
||||
"reset": "Reset to System"
|
||||
},
|
||||
"pageTitle": "Customization"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Failed to load dashboard data",
|
||||
@@ -174,8 +226,43 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Files to be deleted",
|
||||
"foldersToDelete": "Folders to be deleted",
|
||||
"itemsToDelete": "Items to be deleted",
|
||||
"sharesToDelete": "Shares to be deleted"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Download queued: {fileName}",
|
||||
"queuedDescription": "Your download will start automatically when a slot becomes available",
|
||||
"queuePosition": "Download queued at position {position}: {fileName}",
|
||||
"estimatedWait": "Estimated wait time: {time}",
|
||||
"queueFull": "Download queue is full",
|
||||
"queueFullDescription": "Please try again in a few minutes when the queue has space",
|
||||
"cancelSuccess": "Download cancelled successfully",
|
||||
"cancelError": "Failed to cancel download: {error}",
|
||||
"status": {
|
||||
"pending": "Preparing...",
|
||||
"queued": "In queue",
|
||||
"downloading": "Downloading",
|
||||
"completed": "Completed",
|
||||
"failed": "Failed"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}s",
|
||||
"minutes": "{minutes}m",
|
||||
"hoursMinutes": "{hours}h {minutes}m"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Downloads",
|
||||
"downloads": "Download Queue",
|
||||
"active": "Active",
|
||||
"queued": "Queued",
|
||||
"position": "Position {position}",
|
||||
"estimatedWait": "Wait: {time}",
|
||||
"unknownFile": "Unknown file",
|
||||
"noDownloads": "No downloads in progress",
|
||||
"refresh": "Refresh Queue"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "No files uploaded yet",
|
||||
"uploadFile": "Upload File"
|
||||
@@ -201,6 +288,7 @@
|
||||
"extension": "Extension",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Enter file description",
|
||||
"addDescriptionPlaceholder": "Add description...",
|
||||
"deleteFile": "Delete File",
|
||||
"deleteConfirmation": "Are you sure you want to delete this file?",
|
||||
"deleteWarning": "This action cannot be undone."
|
||||
@@ -245,6 +333,7 @@
|
||||
"fileCount": "{count, plural, =1 {file} other {files}}",
|
||||
"filesSelected": "{count, plural, =0 {No files selected} =1 {1 file selected} other {# files selected}}",
|
||||
"editFile": "Edit file",
|
||||
"editFolder": "Edit folder",
|
||||
"previewFile": "Preview file",
|
||||
"addToShare": "Add to share",
|
||||
"removeFromShare": "Remove from share",
|
||||
@@ -265,9 +354,22 @@
|
||||
"bulkDownloadSuccess": "Files download started successfully",
|
||||
"bulkDownloadError": "Error creating ZIP file",
|
||||
"bulkDownloadFileError": "Error downloading file {fileName}",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 file deleted successfully} other {# files deleted successfully}}",
|
||||
"bulkDeleteError": "Error deleting selected files",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 item deleted successfully} other {# items deleted successfully}}",
|
||||
"bulkDeleteError": "Error deleting selected items",
|
||||
"bulkDeleteTitle": "Delete Selected Items",
|
||||
"bulkDeleteConfirmation": "Are you sure you want to delete {count, plural, =1 {1 item} other {# items}}? This action cannot be undone.",
|
||||
"totalFiles": "{count, plural, =0 {No files} =1 {1 file} other {# files}}",
|
||||
"empty": {
|
||||
"title": "No files or folders yet",
|
||||
"description": "Upload your first file or create a folder to get started"
|
||||
},
|
||||
"files": "files",
|
||||
"folders": "folders",
|
||||
"actions": {
|
||||
"open": "Open",
|
||||
"rename": "Rename",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"viewMode": {
|
||||
"table": "Table",
|
||||
"grid": "Grid"
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Delete Selected"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Edit Folder",
|
||||
"folderName": "Folder Name",
|
||||
"folderNamePlaceholder": "Enter folder name",
|
||||
"folderDescription": "Description",
|
||||
"folderDescriptionPlaceholder": "Enter folder description (optional)",
|
||||
"createFolder": "Create Folder",
|
||||
"renameFolder": "Rename Folder",
|
||||
"moveFolder": "Move Folder",
|
||||
"shareFolder": "Share Folder",
|
||||
"deleteFolder": "Delete Folder",
|
||||
"moveTo": "Move to",
|
||||
"selectDestination": "Select destination folder",
|
||||
"rootFolder": "Root",
|
||||
"folderCreated": "Folder created successfully",
|
||||
"folderRenamed": "Folder renamed successfully",
|
||||
"folderMoved": "Folder moved successfully",
|
||||
"folderDeleted": "Folder deleted successfully",
|
||||
"folderShared": "Folder shared successfully",
|
||||
"createFolderError": "Error creating folder",
|
||||
"renameFolderError": "Error renaming folder",
|
||||
"moveFolderError": "Error moving folder",
|
||||
"deleteFolderError": "Error deleting folder",
|
||||
"shareFolderError": "Error sharing folder",
|
||||
"deleteConfirmation": "Are you sure you want to delete this folder?",
|
||||
"deleteWarning": "This action cannot be undone."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Powered by",
|
||||
"kyanHomepage": "Kyantech homepage"
|
||||
@@ -402,10 +531,18 @@
|
||||
"removeFailed": "Failed to remove logo"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Items to move:",
|
||||
"movingTo": "Moving to:",
|
||||
"title": "Move {count, plural, =1 {Item} other {Items}}",
|
||||
"description": "Move {count, plural, =1 {item} other {items}} to a new location",
|
||||
"success": "Successfully moved {count} {count, plural, =1 {item} other {items}}"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "App Logo",
|
||||
"profileMenu": "Profile Menu",
|
||||
"profile": "Profile",
|
||||
"customization": "Customization",
|
||||
"settings": "Settings",
|
||||
"usersManagement": "User Management",
|
||||
"logout": "Log Out"
|
||||
@@ -413,6 +550,24 @@
|
||||
"navigation": {
|
||||
"dashboard": "Dashboard"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Download notifications enabled",
|
||||
"permissionDenied": "Download notifications disabled",
|
||||
"downloadComplete": {
|
||||
"title": "Download Complete",
|
||||
"body": "{fileName} has finished downloading"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Download Failed",
|
||||
"body": "Failed to download {fileName}: {error}",
|
||||
"unknownError": "Unknown error"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Download Starting",
|
||||
"body": "{fileName} is now downloading{position}",
|
||||
"position": " (was #{position} in queue)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Change Password",
|
||||
@@ -750,6 +905,7 @@
|
||||
"noFiles": "No files received yet",
|
||||
"noFilesDescription": "Files sent through this link will appear here",
|
||||
"fileCount": "{count, plural, =0 {No files} =1 {1 file} other {# files}}",
|
||||
"invalidDate": "Invalid date",
|
||||
"totalSize": "Total size: {size}",
|
||||
"columns": {
|
||||
"file": "File",
|
||||
@@ -1007,8 +1163,11 @@
|
||||
}
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Search files...",
|
||||
"results": "Found {filtered} of {total} files"
|
||||
"placeholder": "Search files and folders...",
|
||||
"placeholderFiles": "Search files...",
|
||||
"placeholderFolders": "Search folders...",
|
||||
"results": "Showing {filtered} of {total} items",
|
||||
"noResults": "No results found for \"{query}\""
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1201,19 +1360,20 @@
|
||||
"pageTitle": "Share"
|
||||
},
|
||||
"shareActions": {
|
||||
"fileTitle": "Share File",
|
||||
"folderTitle": "Share Folder",
|
||||
"linkTitle": "Generate Link",
|
||||
"linkDescriptionFile": "Generate a custom link to share the file",
|
||||
"linkDescriptionFolder": "Generate a custom link to share the folder",
|
||||
"aliasLabel": "Link Alias",
|
||||
"aliasPlaceholder": "Enter custom alias",
|
||||
"linkReady": "Your share link is ready:",
|
||||
"generateLink": "Generate Link",
|
||||
"copyLink": "Copy Link",
|
||||
"deleteTitle": "Delete Share",
|
||||
"deleteConfirmation": "Are you sure you want to delete this share? This action cannot be undone.",
|
||||
"addDescriptionPlaceholder": "Add description...",
|
||||
"editTitle": "Edit Share",
|
||||
"nameLabel": "Share Name",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Enter a description (optional)",
|
||||
"expirationLabel": "Expiration Date",
|
||||
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
|
||||
"maxViewsLabel": "Max Views",
|
||||
"maxViewsPlaceholder": "Leave empty for unlimited",
|
||||
"passwordProtection": "Password Protected",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"newPasswordLabel": "New Password (leave empty to keep current)",
|
||||
"newPasswordPlaceholder": "Enter new password",
|
||||
"manageFilesTitle": "Manage Files",
|
||||
@@ -1284,28 +1444,6 @@
|
||||
"noExpiration": "This share will never expire and will remain accessible indefinitely."
|
||||
}
|
||||
},
|
||||
"shareFile": {
|
||||
"title": "Share File",
|
||||
"linkTitle": "Generate Link",
|
||||
"nameLabel": "Share Name",
|
||||
"namePlaceholder": "Enter share name",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Enter a description (optional)",
|
||||
"expirationLabel": "Expiration Date",
|
||||
"expirationPlaceholder": "MM/DD/YYYY HH:MM",
|
||||
"maxViewsLabel": "Maximum Views",
|
||||
"maxViewsPlaceholder": "Leave empty for unlimited",
|
||||
"passwordProtection": "Password Protected",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"linkDescription": "Generate a custom link to share the file",
|
||||
"aliasLabel": "Link Alias",
|
||||
"aliasPlaceholder": "Enter custom alias",
|
||||
"linkReady": "Your share link is ready:",
|
||||
"createShare": "Create Share",
|
||||
"generateLink": "Generate Link",
|
||||
"copyLink": "Copy Link"
|
||||
},
|
||||
"shareManager": {
|
||||
"deleteSuccess": "Share deleted successfully",
|
||||
"deleteError": "Failed to delete share",
|
||||
@@ -1343,11 +1481,12 @@
|
||||
"shareNamePlaceholder": "Enter share name",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Enter a description (optional)",
|
||||
"filesToShare": "Files to share",
|
||||
"filesToShare": "Files to Share",
|
||||
"files": "files",
|
||||
"totalSize": "Total size",
|
||||
"creating": "Creating...",
|
||||
"create": "Create Share"
|
||||
"creating": "Creating share...",
|
||||
"create": "Create Share",
|
||||
"itemsToShare": "Items to share ({count} {count, plural, =1 {item} other {items}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"title": "Share Security Settings",
|
||||
@@ -1430,6 +1569,7 @@
|
||||
"public": "Public"
|
||||
},
|
||||
"filesCount": "files",
|
||||
"folderCount": "folders",
|
||||
"recipientsCount": "recipients",
|
||||
"actions": {
|
||||
"menu": "Share actions menu",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "Actualizar",
|
||||
"click": "Haga clic para",
|
||||
"creating": "Creando...",
|
||||
"loadingSimple": "Cargando..."
|
||||
"loadingSimple": "Cargando...",
|
||||
"create": "Crear",
|
||||
"deleting": "Eliminando...",
|
||||
"move": "Mover",
|
||||
"rename": "Renombrar",
|
||||
"search": "Buscar",
|
||||
"share": "Compartir"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Crear Compartir",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "Crear Compartir",
|
||||
"success": "Compartir creado exitosamente",
|
||||
"error": "Error al crear compartir",
|
||||
"namePlaceholder": "Ingrese un nombre para su compartir"
|
||||
"namePlaceholder": "Ingrese un nombre para su compartir",
|
||||
"nextSelectFiles": "Siguiente: Seleccionar archivos",
|
||||
"searchLabel": "Buscar",
|
||||
"tabs": {
|
||||
"shareDetails": "Detalles del compartido",
|
||||
"selectFiles": "Seleccionar archivos"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Personalización",
|
||||
"colors": {
|
||||
"title": "Colores del Tema",
|
||||
"description": "Elige tu color primario preferido para el tema",
|
||||
"presets": "Colores Disponibles",
|
||||
"presetsDescription": "Selecciona entre los temas de colores disponibles",
|
||||
"reset": "Restablecer por Defecto"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Tipografía",
|
||||
"description": "Elige tu familia de fuentes preferida",
|
||||
"available": "Fuentes Disponibles",
|
||||
"availableDescription": "Selecciona entre las familias de fuentes disponibles",
|
||||
"reset": "Restablecer por Defecto"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Radio del Borde",
|
||||
"description": "Personaliza la redondez de los elementos de la interfaz",
|
||||
"available": "Opciones de Redondez",
|
||||
"availableDescription": "Elige cómo deben aparecer las esquinas redondeadas",
|
||||
"reset": "Restablecer por Defecto"
|
||||
},
|
||||
"background": {
|
||||
"title": "Colores de Fondo",
|
||||
"description": "Personaliza los colores de fondo para los modos claro y oscuro",
|
||||
"lightMode": "Modo Claro",
|
||||
"darkMode": "Modo Oscuro",
|
||||
"availableDescription": "Elige los colores de fondo para los temas claro y oscuro",
|
||||
"reset": "Restablecer por Defecto"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Modo del Tema",
|
||||
"description": "Elige entre tema claro, oscuro o del sistema",
|
||||
"selectTheme": "Preferencia de Tema",
|
||||
"availableDescription": "Selecciona tu modo de tema preferido",
|
||||
"reset": "Restablecer al Sistema"
|
||||
},
|
||||
"pageTitle": "Personalización"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Error al cargar los datos del tablero",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Archivos que serán eliminados",
|
||||
"sharesToDelete": "Compartidos que serán eliminados"
|
||||
"sharesToDelete": "Compartidos que serán eliminados",
|
||||
"foldersToDelete": "Carpetas a eliminar",
|
||||
"itemsToDelete": "Elementos a eliminar"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Descarga en cola: {fileName}",
|
||||
"queuedDescription": "Tu descarga comenzará automáticamente cuando haya un espacio disponible",
|
||||
"queuePosition": "Descarga en cola en posición {position}: {fileName}",
|
||||
"estimatedWait": "Tiempo estimado de espera: {time}",
|
||||
"queueFull": "Cola de descarga llena",
|
||||
"queueFullDescription": "Por favor, inténtalo de nuevo en unos minutos cuando la cola tenga espacio",
|
||||
"cancelSuccess": "Descarga cancelada exitosamente",
|
||||
"cancelError": "Error al cancelar la descarga: {error}",
|
||||
"status": {
|
||||
"pending": "Preparando...",
|
||||
"queued": "En cola",
|
||||
"downloading": "Descargando",
|
||||
"completed": "Completado",
|
||||
"failed": "Fallido"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}s",
|
||||
"minutes": "{minutes}m",
|
||||
"hoursMinutes": "{hours}h {minutes}m"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Descargas",
|
||||
"downloads": "Cola de Descargas",
|
||||
"active": "Activas",
|
||||
"queued": "En Cola",
|
||||
"position": "Posición {position}",
|
||||
"estimatedWait": "Espera: {time}",
|
||||
"unknownFile": "Archivo desconocido",
|
||||
"noDownloads": "No hay descargas en progreso",
|
||||
"refresh": "Actualizar Cola"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Aún no se han subido archivos",
|
||||
@@ -201,6 +288,7 @@
|
||||
"extension": "Extensión",
|
||||
"descriptionLabel": "Descripción",
|
||||
"descriptionPlaceholder": "Introduce una descripción del archivo",
|
||||
"addDescriptionPlaceholder": "Agregar descripción...",
|
||||
"deleteFile": "Eliminar archivo",
|
||||
"deleteConfirmation": "¿Estás seguro de que deseas eliminar ?",
|
||||
"deleteWarning": "Esta acción no se puede deshacer."
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Vista previa del archivo",
|
||||
"addToShare": "Agregar a compartición",
|
||||
"removeFromShare": "Quitar de compartición",
|
||||
"saveChanges": "Guardar Cambios"
|
||||
"saveChanges": "Guardar Cambios",
|
||||
"editFolder": "Editar carpeta"
|
||||
},
|
||||
"files": {
|
||||
"title": "Todos los Archivos",
|
||||
@@ -267,11 +356,24 @@
|
||||
"bulkDownloadFileError": "Error al descargar archivo {fileName}",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 archivo eliminado exitosamente} other {# archivos eliminados exitosamente}}",
|
||||
"bulkDeleteError": "Error al eliminar archivos seleccionados",
|
||||
"bulkDeleteTitle": "Eliminar Archivos Seleccionados",
|
||||
"bulkDeleteConfirmation": "¿Está seguro de que desea eliminar {count, plural, =1 {1 archivo} other {# archivos}}? Esta acción no se puede deshacer.",
|
||||
"viewMode": {
|
||||
"table": "Tabla",
|
||||
"grid": "Cuadrícula"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {Sin archivos} =1 {1 archivo} other {# archivos}}"
|
||||
"totalFiles": "{count, plural, =0 {Sin archivos} =1 {1 archivo} other {# archivos}}",
|
||||
"actions": {
|
||||
"open": "Abrir",
|
||||
"rename": "Renombrar",
|
||||
"delete": "Eliminar"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Aún no hay archivos o carpetas",
|
||||
"description": "Suba su primer archivo o cree una carpeta para comenzar"
|
||||
},
|
||||
"files": "archivos",
|
||||
"folders": "carpetas"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Tabla de archivos",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Eliminar Seleccionados"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Editar carpeta",
|
||||
"folderName": "Nombre de carpeta",
|
||||
"folderNamePlaceholder": "Ingrese nombre de carpeta",
|
||||
"folderDescription": "Descripción",
|
||||
"folderDescriptionPlaceholder": "Ingrese descripción de carpeta (opcional)",
|
||||
"createFolder": "Crear nueva carpeta",
|
||||
"renameFolder": "Renombrar carpeta",
|
||||
"moveFolder": "Mover carpeta",
|
||||
"shareFolder": "Compartir carpeta",
|
||||
"deleteFolder": "Eliminar carpeta",
|
||||
"moveTo": "Mover a",
|
||||
"selectDestination": "Seleccionar carpeta destino",
|
||||
"rootFolder": "Raíz",
|
||||
"folderCreated": "Carpeta creada exitosamente",
|
||||
"folderRenamed": "Carpeta renombrada exitosamente",
|
||||
"folderMoved": "Carpeta movida exitosamente",
|
||||
"folderDeleted": "Carpeta eliminada exitosamente",
|
||||
"folderShared": "Carpeta compartida exitosamente",
|
||||
"createFolderError": "Error al crear carpeta",
|
||||
"renameFolderError": "Error al renombrar carpeta",
|
||||
"moveFolderError": "Error al mover carpeta",
|
||||
"deleteFolderError": "Error al eliminar carpeta",
|
||||
"shareFolderError": "Error al compartir carpeta",
|
||||
"deleteConfirmation": "¿Está seguro de que desea eliminar esta carpeta?",
|
||||
"deleteWarning": "Esta acción no se puede deshacer."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Desarrollado por",
|
||||
"kyanHomepage": "Página principal de Kyantech"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Error al eliminar el logo"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Elementos a mover:",
|
||||
"movingTo": "Moviendo a:",
|
||||
"title": "Mover {count, plural, =1 {elemento} other {elementos}}",
|
||||
"description": "Mover {count, plural, =1 {elemento} other {elementos}} a una nueva ubicación",
|
||||
"success": "Se movieron exitosamente {count} {count, plural, =1 {elemento} other {elementos}}"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Logo de la aplicación",
|
||||
"profileMenu": "Menú de perfil",
|
||||
"profile": "Perfil",
|
||||
"settings": "Configuración",
|
||||
"usersManagement": "Gestión de usuarios",
|
||||
"logout": "Cerrar sesión"
|
||||
"logout": "Cerrar sesión",
|
||||
"customization": "Personalización"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Panel de control"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Notificaciones de descarga habilitadas",
|
||||
"permissionDenied": "Notificaciones de descarga deshabilitadas",
|
||||
"downloadComplete": {
|
||||
"title": "Descarga Completada",
|
||||
"body": "{fileName} ha terminado de descargarse"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Descarga Fallida",
|
||||
"body": "Error al descargar {fileName}: {error}",
|
||||
"unknownError": "Error desconocido"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Descarga Iniciando",
|
||||
"body": "{fileName} está descargándose ahora{position}",
|
||||
"position": " (estaba en posición #{position} en la cola)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Cambiar contraseña",
|
||||
@@ -750,6 +905,7 @@
|
||||
"noFiles": "Ningún archivo recibido aún",
|
||||
"noFilesDescription": "Los archivos enviados a través de este enlace aparecerán aquí",
|
||||
"fileCount": "{count, plural, =0 {Ningún archivo} =1 {1 archivo} other {# archivos}}",
|
||||
"invalidDate": "Fecha inválida",
|
||||
"totalSize": "Tamaño total: {size}",
|
||||
"columns": {
|
||||
"file": "Archivo",
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Buscar archivos...",
|
||||
"results": "Se encontraron {filtered} de {total} archivos"
|
||||
"results": "Se encontraron {filtered} de {total} archivos",
|
||||
"placeholderFolders": "Buscar carpetas...",
|
||||
"noResults": "No se encontraron resultados para \"{query}\"",
|
||||
"placeholderFiles": "Buscar archivos..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1205,6 +1364,7 @@
|
||||
"shareActions": {
|
||||
"deleteTitle": "Eliminar Compartir",
|
||||
"deleteConfirmation": "¿Estás seguro de que deseas eliminar esta compartición? Esta acción no se puede deshacer.",
|
||||
"addDescriptionPlaceholder": "Agregar descripción...",
|
||||
"editTitle": "Editar Compartir",
|
||||
"nameLabel": "Nombre del Compartir",
|
||||
"descriptionLabel": "Descripción",
|
||||
@@ -1223,7 +1383,17 @@
|
||||
"editSuccess": "Compartir actualizado exitosamente",
|
||||
"editError": "Error al actualizar compartir",
|
||||
"bulkDeleteConfirmation": "¿Estás seguro de que quieres eliminar {count, plural, =1 {1 compartido} other {# compartidos}}? Esta acción no se puede deshacer.",
|
||||
"bulkDeleteTitle": "Eliminar Compartidos Seleccionados"
|
||||
"bulkDeleteTitle": "Eliminar Compartidos Seleccionados",
|
||||
"aliasLabel": "Alias del enlace",
|
||||
"aliasPlaceholder": "Ingrese alias personalizado",
|
||||
"copyLink": "Copiar enlace",
|
||||
"fileTitle": "Compartir archivo",
|
||||
"folderTitle": "Compartir carpeta",
|
||||
"generateLink": "Generar enlace",
|
||||
"linkDescriptionFile": "Genere un enlace personalizado para compartir el archivo",
|
||||
"linkDescriptionFolder": "Genere un enlace personalizado para compartir la carpeta",
|
||||
"linkReady": "Su enlace de compartición está listo:",
|
||||
"linkTitle": "Generar enlace"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Detalles del Compartir",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "archivos",
|
||||
"totalSize": "Tamaño total",
|
||||
"creating": "Creando...",
|
||||
"create": "Crear Compartir"
|
||||
"create": "Crear Compartir",
|
||||
"itemsToShare": "Elementos a compartir ({count} {count, plural, =1 {elemento} other {elementos}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "Configurar protección por contraseña y opciones de seguridad para este compartir",
|
||||
@@ -1433,6 +1604,7 @@
|
||||
"public": "Público"
|
||||
},
|
||||
"filesCount": "archivos",
|
||||
"folderCount": "carpetas",
|
||||
"recipientsCount": "destinatarios",
|
||||
"actions": {
|
||||
"menu": "Menú de acciones de compartir",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "Mettre à jour",
|
||||
"click": "Clique para",
|
||||
"creating": "Criando...",
|
||||
"loadingSimple": "Chargement..."
|
||||
"loadingSimple": "Chargement...",
|
||||
"create": "Créer",
|
||||
"deleting": "Suppression...",
|
||||
"move": "Déplacer",
|
||||
"rename": "Renommer",
|
||||
"search": "Rechercher",
|
||||
"share": "Partager"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Créer un Partage",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "Créer un Partage",
|
||||
"success": "Partage créé avec succès",
|
||||
"error": "Échec de la création du partage",
|
||||
"namePlaceholder": "Entrez un nom pour votre partage"
|
||||
"namePlaceholder": "Entrez un nom pour votre partage",
|
||||
"nextSelectFiles": "Suivant : Sélectionner les fichiers",
|
||||
"searchLabel": "Rechercher",
|
||||
"tabs": {
|
||||
"shareDetails": "Détails du partage",
|
||||
"selectFiles": "Sélectionner les fichiers"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Personnalisation",
|
||||
"colors": {
|
||||
"title": "Couleurs du Thème",
|
||||
"description": "Choisissez votre thème de couleur principale préféré",
|
||||
"presets": "Couleurs Disponibles",
|
||||
"presetsDescription": "Sélectionnez parmi les thèmes de couleurs disponibles",
|
||||
"reset": "Réinitialiser par Défaut"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Typographie",
|
||||
"description": "Choisissez votre famille de polices préférée",
|
||||
"available": "Polices Disponibles",
|
||||
"availableDescription": "Sélectionnez parmi les familles de polices disponibles",
|
||||
"reset": "Réinitialiser par Défaut"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Rayon des Bordures",
|
||||
"description": "Personnalisez l'arrondi des éléments de l'interface",
|
||||
"available": "Options d'Arrondi",
|
||||
"availableDescription": "Choisissez l'apparence des coins arrondis",
|
||||
"reset": "Réinitialiser par Défaut"
|
||||
},
|
||||
"background": {
|
||||
"title": "Couleurs d'Arrière-plan",
|
||||
"description": "Personnalisez les couleurs d'arrière-plan pour les modes clair et sombre",
|
||||
"lightMode": "Mode Clair",
|
||||
"darkMode": "Mode Sombre",
|
||||
"availableDescription": "Choisissez les couleurs d'arrière-plan pour les thèmes clair et sombre",
|
||||
"reset": "Réinitialiser par Défaut"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Mode de Thème",
|
||||
"description": "Choisissez entre le thème clair, sombre ou système",
|
||||
"selectTheme": "Préférence de Thème",
|
||||
"availableDescription": "Sélectionnez votre mode de thème préféré",
|
||||
"reset": "Réinitialiser au Système"
|
||||
},
|
||||
"pageTitle": "Personnalisation"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Échec du chargement des données du tableau de bord",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Fichiers à supprimer",
|
||||
"sharesToDelete": "Partages qui seront supprimés"
|
||||
"sharesToDelete": "Partages qui seront supprimés",
|
||||
"foldersToDelete": "Dossiers à supprimer",
|
||||
"itemsToDelete": "Éléments à supprimer"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Téléchargement en file d'attente : {fileName}",
|
||||
"queuedDescription": "Votre téléchargement démarrera automatiquement lorsqu'un emplacement sera disponible",
|
||||
"queuePosition": "Téléchargement en position {position} : {fileName}",
|
||||
"estimatedWait": "Temps d'attente estimé : {time}",
|
||||
"queueFull": "La file d'attente est pleine",
|
||||
"queueFullDescription": "Veuillez réessayer dans quelques minutes lorsque la file d'attente aura de l'espace",
|
||||
"cancelSuccess": "Téléchargement annulé avec succès",
|
||||
"cancelError": "Échec de l'annulation du téléchargement : {error}",
|
||||
"status": {
|
||||
"pending": "En préparation...",
|
||||
"queued": "En file d'attente",
|
||||
"downloading": "Téléchargement en cours",
|
||||
"completed": "Terminé",
|
||||
"failed": "Échoué"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}s",
|
||||
"minutes": "{minutes}m",
|
||||
"hoursMinutes": "{hours}h {minutes}m"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Téléchargements",
|
||||
"downloads": "File d'Attente des Téléchargements",
|
||||
"active": "Actif",
|
||||
"queued": "En Attente",
|
||||
"position": "Position {position}",
|
||||
"estimatedWait": "Attente : {time}",
|
||||
"unknownFile": "Fichier inconnu",
|
||||
"noDownloads": "Aucun téléchargement en cours",
|
||||
"refresh": "Actualiser la File d'Attente"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Aucun fichier téléchargé pour le moment",
|
||||
@@ -201,6 +288,7 @@
|
||||
"extension": "Extension",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Entrez la description du fichier",
|
||||
"addDescriptionPlaceholder": "Ajouter une description...",
|
||||
"deleteFile": "Supprimer le Fichier",
|
||||
"deleteConfirmation": "Êtes-vous sûr de vouloir supprimer ?",
|
||||
"deleteWarning": "Cette action ne peut pas être annulée."
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Aperçu du fichier",
|
||||
"addToShare": "Ajouter au partage",
|
||||
"removeFromShare": "Retirer du partage",
|
||||
"saveChanges": "Sauvegarder les Modifications"
|
||||
"saveChanges": "Sauvegarder les Modifications",
|
||||
"editFolder": "Modifier le dossier"
|
||||
},
|
||||
"files": {
|
||||
"title": "Tous les Fichiers",
|
||||
@@ -267,11 +356,24 @@
|
||||
"bulkDownloadFileError": "Erreur lors du téléchargement du fichier {fileName}",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 fichier supprimé avec succès} other {# fichiers supprimés avec succès}}",
|
||||
"bulkDeleteError": "Erreur lors de la suppression des fichiers sélectionnés",
|
||||
"bulkDeleteTitle": "Supprimer les Fichiers Sélectionnés",
|
||||
"bulkDeleteConfirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, =1 {1 fichier} other {# fichiers}} ? Cette action ne peut pas être annulée.",
|
||||
"viewMode": {
|
||||
"table": "Tableau",
|
||||
"grid": "Grille"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {Aucun fichier} =1 {1 fichier} other {# fichiers}}"
|
||||
"totalFiles": "{count, plural, =0 {Aucun fichier} =1 {1 fichier} other {# fichiers}}",
|
||||
"actions": {
|
||||
"open": "Ouvrir",
|
||||
"rename": "Renommer",
|
||||
"delete": "Supprimer"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Aucun fichier ou dossier pour le moment",
|
||||
"description": "Téléchargez votre premier fichier ou créez un dossier pour commencer"
|
||||
},
|
||||
"files": "fichiers",
|
||||
"folders": "dossiers"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Tableau des fichiers",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Supprimer les Sélectionnés"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Modifier le dossier",
|
||||
"folderName": "Nom du dossier",
|
||||
"folderNamePlaceholder": "Entrez le nom du dossier",
|
||||
"folderDescription": "Description",
|
||||
"folderDescriptionPlaceholder": "Entrez la description du dossier (facultatif)",
|
||||
"createFolder": "Créer un nouveau dossier",
|
||||
"renameFolder": "Renommer le dossier",
|
||||
"moveFolder": "Déplacer le dossier",
|
||||
"shareFolder": "Partager le dossier",
|
||||
"deleteFolder": "Supprimer le dossier",
|
||||
"moveTo": "Déplacer vers",
|
||||
"selectDestination": "Sélectionner le dossier de destination",
|
||||
"rootFolder": "Racine",
|
||||
"folderCreated": "Dossier créé avec succès",
|
||||
"folderRenamed": "Dossier renommé avec succès",
|
||||
"folderMoved": "Dossier déplacé avec succès",
|
||||
"folderDeleted": "Dossier supprimé avec succès",
|
||||
"folderShared": "Dossier partagé avec succès",
|
||||
"createFolderError": "Erreur lors de la création du dossier",
|
||||
"renameFolderError": "Erreur lors du renommage du dossier",
|
||||
"moveFolderError": "Erreur lors du déplacement du dossier",
|
||||
"deleteFolderError": "Erreur lors de la suppression du dossier",
|
||||
"shareFolderError": "Erreur lors du partage du dossier",
|
||||
"deleteConfirmation": "Êtes-vous sûr de vouloir supprimer ce dossier ?",
|
||||
"deleteWarning": "Cette action ne peut pas être annulée."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Propulsé par",
|
||||
"kyanHomepage": "Page d'accueil de Kyantech"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Échec de la suppression du logo"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Éléments à déplacer :",
|
||||
"movingTo": "Déplacement vers :",
|
||||
"title": "Déplacer {count, plural, =1 {élément} other {éléments}}",
|
||||
"description": "Déplacer {count, plural, =1 {élément} other {éléments}} vers un nouvel emplacement",
|
||||
"success": "{count} {count, plural, =1 {élément déplacé} other {éléments déplacés}} avec succès"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Logo de l'Application",
|
||||
"profileMenu": "Menu du Profil",
|
||||
"profile": "Profil",
|
||||
"settings": "Paramètres",
|
||||
"usersManagement": "Gestion des Utilisateurs",
|
||||
"logout": "Déconnexion"
|
||||
"logout": "Déconnexion",
|
||||
"customization": "Personnalisation"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Tableau de bord"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Notifications de téléchargement activées",
|
||||
"permissionDenied": "Notifications de téléchargement désactivées",
|
||||
"downloadComplete": {
|
||||
"title": "Téléchargement Terminé",
|
||||
"body": "{fileName} a fini de télécharger"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Échec du Téléchargement",
|
||||
"body": "Échec du téléchargement de {fileName} : {error}",
|
||||
"unknownError": "Erreur inconnue"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Démarrage du Téléchargement",
|
||||
"body": "{fileName} est en cours de téléchargement{position}",
|
||||
"position": " (était n°{position} dans la file d'attente)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Changer le Mot de Passe",
|
||||
@@ -750,6 +905,7 @@
|
||||
"noFiles": "Aucun fichier reçu pour le moment",
|
||||
"noFilesDescription": "Les fichiers envoyés via ce lien apparaîtront ici",
|
||||
"fileCount": "{count, plural, =0 {Aucun fichier} =1 {1 fichier} other {# fichiers}}",
|
||||
"invalidDate": "Date invalide",
|
||||
"totalSize": "Taille totale : {size}",
|
||||
"columns": {
|
||||
"file": "Fichier",
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Rechercher des fichiers...",
|
||||
"results": "Trouvé {filtered} sur {total} fichiers"
|
||||
"results": "Trouvé {filtered} sur {total} fichiers",
|
||||
"placeholderFolders": "Rechercher des dossiers...",
|
||||
"noResults": "Aucun résultat trouvé pour \"{query}\"",
|
||||
"placeholderFiles": "Rechercher des fichiers..."
|
||||
},
|
||||
"settings": {
|
||||
"title": "Paramètres",
|
||||
@@ -1205,6 +1364,7 @@
|
||||
"shareActions": {
|
||||
"deleteTitle": "Supprimer le Partage",
|
||||
"deleteConfirmation": "Êtes-vous sûr de vouloir supprimer ce partage ? Cette action ne peut pas être annulée.",
|
||||
"addDescriptionPlaceholder": "Ajouter une description...",
|
||||
"editTitle": "Modifier le Partage",
|
||||
"nameLabel": "Nom du Partage",
|
||||
"descriptionLabel": "Description",
|
||||
@@ -1223,7 +1383,17 @@
|
||||
"editSuccess": "Partage mis à jour avec succès",
|
||||
"editError": "Échec de la mise à jour du partage",
|
||||
"bulkDeleteConfirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, =1 {1 partage} other {# partages}} ? Cette action ne peut pas être annulée.",
|
||||
"bulkDeleteTitle": "Supprimer les Partages Sélectionnés"
|
||||
"bulkDeleteTitle": "Supprimer les Partages Sélectionnés",
|
||||
"aliasLabel": "Alias du lien",
|
||||
"aliasPlaceholder": "Entrez un alias personnalisé",
|
||||
"copyLink": "Copier le lien",
|
||||
"fileTitle": "Partager le fichier",
|
||||
"folderTitle": "Partager le dossier",
|
||||
"generateLink": "Générer un lien",
|
||||
"linkDescriptionFile": "Générez un lien personnalisé pour partager le fichier",
|
||||
"linkDescriptionFolder": "Générez un lien personnalisé pour partager le dossier",
|
||||
"linkReady": "Votre lien de partage est prêt :",
|
||||
"linkTitle": "Générer un lien"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Détails du Partage",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "fichiers",
|
||||
"totalSize": "Taille totale",
|
||||
"creating": "Création...",
|
||||
"create": "Créer un Partage"
|
||||
"create": "Créer un Partage",
|
||||
"itemsToShare": "Éléments à partager ({count} {count, plural, =1 {élément} other {éléments}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "Configurer la protection par mot de passe et les options de sécurité pour ce partage",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "Télécharger sélectionné"
|
||||
},
|
||||
"selectAll": "Tout sélectionner",
|
||||
"selectShare": "Sélectionner le partage {shareName}"
|
||||
"selectShare": "Sélectionner le partage {shareName}",
|
||||
"folderCount": "dossiers"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "Utilisation du Stockage",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "अपडेट करें",
|
||||
"click": "क्लिक करें",
|
||||
"creating": "बना रहा है...",
|
||||
"loadingSimple": "लोड हो रहा है..."
|
||||
"loadingSimple": "लोड हो रहा है...",
|
||||
"create": "बनाएं",
|
||||
"deleting": "हटा रहे हैं...",
|
||||
"move": "स्थानांतरित करें",
|
||||
"rename": "नाम बदलें",
|
||||
"search": "खोजें",
|
||||
"share": "साझा करें"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "साझाकरण बनाएं",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "साझाकरण बनाएं",
|
||||
"success": "साझाकरण सफलतापूर्वक बनाया गया",
|
||||
"error": "साझाकरण बनाने में विफल",
|
||||
"namePlaceholder": "अपने साझाकरण के लिए एक नाम दर्ज करें"
|
||||
"namePlaceholder": "अपने साझाकरण के लिए एक नाम दर्ज करें",
|
||||
"nextSelectFiles": "आगे: फ़ाइलें चुनें",
|
||||
"searchLabel": "खोजें",
|
||||
"tabs": {
|
||||
"shareDetails": "साझाकरण विवरण",
|
||||
"selectFiles": "फ़ाइलें चुनें"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "अनुकूलन",
|
||||
"colors": {
|
||||
"title": "थीम रंग",
|
||||
"description": "अपना पसंदीदा प्राथमिक रंग थीम चुनें",
|
||||
"presets": "उपलब्ध रंग",
|
||||
"presetsDescription": "उपलब्ध रंग थीम में से चुनें",
|
||||
"reset": "डिफ़ॉल्ट पर रीसेट करें"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "टाइपोग्राफी",
|
||||
"description": "अपना पसंदीदा फ़ॉन्ट परिवार चुनें",
|
||||
"available": "उपलब्ध फ़ॉन्ट",
|
||||
"availableDescription": "उपलब्ध फ़ॉन्ट परिवारों में से चुनें",
|
||||
"reset": "डिफ़ॉल्ट पर रीसेट करें"
|
||||
},
|
||||
"radius": {
|
||||
"title": "बॉर्डर रेडियस",
|
||||
"description": "इंटरफ़ेस तत्वों की गोलाई को अनुकूलित करें",
|
||||
"available": "गोलाई विकल्प",
|
||||
"availableDescription": "कोने कितने गोल दिखाई दें यह चुनें",
|
||||
"reset": "डिफ़ॉल्ट पर रीसेट करें"
|
||||
},
|
||||
"background": {
|
||||
"title": "पृष्ठभूमि रंग",
|
||||
"description": "लाइट और डार्क मोड के लिए पृष्ठभूमि रंग अनुकूलित करें",
|
||||
"lightMode": "लाइट मोड",
|
||||
"darkMode": "डार्क मोड",
|
||||
"availableDescription": "लाइट और डार्क थीम दोनों के लिए पृष्ठभूमि रंग चुनें",
|
||||
"reset": "डिफ़ॉल्ट पर रीसेट करें"
|
||||
},
|
||||
"theme": {
|
||||
"title": "थीम मोड",
|
||||
"description": "लाइट, डार्क या सिस्टम थीम में से चुनें",
|
||||
"selectTheme": "थीम प्राथमिकता",
|
||||
"availableDescription": "अपना पसंदीदा थीम मोड चुनें",
|
||||
"reset": "सिस्टम पर रीसेट करें"
|
||||
},
|
||||
"pageTitle": "अनुकूलन"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "डैशबोर्ड डेटा लोड करने में त्रुटि",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "हटाई जाने वाली फाइलें",
|
||||
"sharesToDelete": "साझाकरण जो हटाए जाएंगे"
|
||||
"sharesToDelete": "साझाकरण जो हटाए जाएंगे",
|
||||
"foldersToDelete": "हटाए जाने वाले फ़ोल्डर",
|
||||
"itemsToDelete": "हटाए जाने वाले आइटम"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "डाउनलोड कतार में: {fileName}",
|
||||
"queuedDescription": "जब स्लॉट उपलब्ध होगा तब आपका डाउनलोड स्वचालित रूप से शुरू हो जाएगा",
|
||||
"queuePosition": "स्थिति {position} पर डाउनलोड कतारबद्ध: {fileName}",
|
||||
"estimatedWait": "अनुमानित प्रतीक्षा समय: {time}",
|
||||
"queueFull": "डाउनलोड कतार भरी हुई है",
|
||||
"queueFullDescription": "कृपया कुछ मिनटों में फिर से प्रयास करें जब कतार में जगह हो",
|
||||
"cancelSuccess": "डाउनलोड सफलतापूर्वक रद्द किया गया",
|
||||
"cancelError": "डाउनलोड रद्द करने में विफल: {error}",
|
||||
"status": {
|
||||
"pending": "तैयारी हो रही है...",
|
||||
"queued": "कतार में",
|
||||
"downloading": "डाउनलोड हो रहा है",
|
||||
"completed": "पूर्ण",
|
||||
"failed": "विफल"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}से",
|
||||
"minutes": "{minutes}मि",
|
||||
"hoursMinutes": "{hours}घं {minutes}मि"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "डाउनलोड",
|
||||
"downloads": "डाउनलोड कतार",
|
||||
"active": "सक्रिय",
|
||||
"queued": "कतारबद्ध",
|
||||
"position": "स्थिति {position}",
|
||||
"estimatedWait": "प्रतीक्षा: {time}",
|
||||
"unknownFile": "अज्ञात फ़ाइल",
|
||||
"noDownloads": "कोई डाउनलोड प्रगति में नहीं है",
|
||||
"refresh": "कतार रीफ्रेश करें"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "अभी तक कोई फाइल अपलोड नहीं हुई",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "फाइल का विवरण दर्ज करें",
|
||||
"deleteFile": "फाइल हटाएं",
|
||||
"deleteConfirmation": "क्या आप वाकई हटाना चाहते हैं?",
|
||||
"deleteWarning": "यह क्रिया अपरिवर्तनीय है।"
|
||||
"deleteWarning": "यह क्रिया अपरिवर्तनीय है।",
|
||||
"addDescriptionPlaceholder": "विवरण जोड़ें..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "फाइल डाउनलोड करने में त्रुटि",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "फाइल पूर्वावलोकन",
|
||||
"addToShare": "साझाकरण में जोड़ें",
|
||||
"removeFromShare": "साझाकरण से हटाएं",
|
||||
"saveChanges": "परिवर्तन सहेजें"
|
||||
"saveChanges": "परिवर्तन सहेजें",
|
||||
"editFolder": "फ़ोल्डर संपादित करें"
|
||||
},
|
||||
"files": {
|
||||
"title": "सभी फाइलें",
|
||||
@@ -271,7 +360,20 @@
|
||||
"table": "तालिका",
|
||||
"grid": "ग्रिड"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {कोई फ़ाइल नहीं} =1 {1 फ़ाइल} other {# फ़ाइलें}}"
|
||||
"totalFiles": "{count, plural, =0 {कोई फ़ाइल नहीं} =1 {1 फ़ाइल} other {# फ़ाइलें}}",
|
||||
"bulkDeleteConfirmation": "क्या आप वास्तव में {count, plural, =1 {1 फाइल} other {# फाइलों}} को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।",
|
||||
"bulkDeleteTitle": "चयनित फाइलों को हटाएं",
|
||||
"actions": {
|
||||
"open": "खोलें",
|
||||
"rename": "नाम बदलें",
|
||||
"delete": "हटाएं"
|
||||
},
|
||||
"empty": {
|
||||
"title": "अभी तक कोई फ़ाइल या फ़ोल्डर नहीं",
|
||||
"description": "आरंभ करने के लिए अपनी पहली फ़ाइल अपलोड करें या फ़ोल्डर बनाएं"
|
||||
},
|
||||
"files": "फ़ाइलें",
|
||||
"folders": "फ़ोल्डर"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "फाइल तालिका",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "चयनित हटाएं"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "फ़ोल्डर संपादित करें",
|
||||
"folderName": "फ़ोल्डर नाम",
|
||||
"folderNamePlaceholder": "फ़ोल्डर नाम दर्ज करें",
|
||||
"folderDescription": "विवरण",
|
||||
"folderDescriptionPlaceholder": "फ़ोल्डर विवरण दर्ज करें (वैकल्पिक)",
|
||||
"createFolder": "नया फ़ोल्डर बनाएं",
|
||||
"renameFolder": "फ़ोल्डर का नाम बदलें",
|
||||
"moveFolder": "फ़ोल्डर स्थानांतरित करें",
|
||||
"shareFolder": "फ़ोल्डर साझा करें",
|
||||
"deleteFolder": "फ़ोल्डर हटाएं",
|
||||
"moveTo": "यहाँ स्थानांतरित करें",
|
||||
"selectDestination": "गंतव्य फ़ोल्डर चुनें",
|
||||
"rootFolder": "मूल",
|
||||
"folderCreated": "फ़ोल्डर सफलतापूर्वक बनाया गया",
|
||||
"folderRenamed": "फ़ोल्डर का नाम सफलतापूर्वक बदला गया",
|
||||
"folderMoved": "फ़ोल्डर सफलतापूर्वक स्थानांतरित किया गया",
|
||||
"folderDeleted": "फ़ोल्डर सफलतापूर्वक हटाया गया",
|
||||
"folderShared": "फ़ोल्डर सफलतापूर्वक साझा किया गया",
|
||||
"createFolderError": "फ़ोल्डर बनाने में त्रुटि",
|
||||
"renameFolderError": "फ़ोल्डर का नाम बदलने में त्रुटि",
|
||||
"moveFolderError": "फ़ोल्डर स्थानांतरित करने में त्रुटि",
|
||||
"deleteFolderError": "फ़ोल्डर हटाने में त्रुटि",
|
||||
"shareFolderError": "फ़ोल्डर साझा करने में त्रुटि",
|
||||
"deleteConfirmation": "क्या आप वाकई इस फ़ोल्डर को हटाना चाहते हैं?",
|
||||
"deleteWarning": "यह कार्य पूर्ववत नहीं किया जा सकता।"
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "द्वारा संचालित",
|
||||
"kyanHomepage": "Kyantech होमपेज"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "लोगो हटाने में विफल"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "स्थानांतरित करने वाले आइटम:",
|
||||
"movingTo": "यहाँ स्थानांतरित कर रहे हैं:",
|
||||
"title": "आइटम स्थानांतरित करें",
|
||||
"description": "आइटम को नए स्थान पर स्थानांतरित करें",
|
||||
"success": "{count} आइटम सफलतापूर्वक स्थानांतरित किए गए"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "एप्लिकेशन लोगो",
|
||||
"profileMenu": "प्रोफ़ाइल मेन्यू",
|
||||
"profile": "प्रोफ़ाइल",
|
||||
"settings": "सेटिंग्स",
|
||||
"usersManagement": "उपयोगकर्ता प्रबंधन",
|
||||
"logout": "लॉग आउट"
|
||||
"logout": "लॉग आउट",
|
||||
"customization": "अनुकूलन"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "डैशबोर्ड"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "डाउनलोड सूचनाएं सक्षम की गईं",
|
||||
"permissionDenied": "डाउनलोड सूचनाएं अक्षम की गईं",
|
||||
"downloadComplete": {
|
||||
"title": "डाउनलोड पूर्ण",
|
||||
"body": "{fileName} का डाउनलोड समाप्त हो गया है"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "डाउनलोड विफल",
|
||||
"body": "{fileName} डाउनलोड करने में विफल: {error}",
|
||||
"unknownError": "अज्ञात त्रुटि"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "डाउनलोड प्रारंभ",
|
||||
"body": "{fileName} अब डाउनलोड हो रहा है{position}",
|
||||
"position": " (कतार में #{position} था)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "पासवर्ड बदलें",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "कॉपी ऑपरेशन का समय समाप्त हो गया। कृपया छोटी फ़ाइल के साथ पुनः प्रयास करें या अपना कनेक्शन जांचें।",
|
||||
"failed": "कॉपी ऑपरेशन विफल हो गया। कृपया पुनः प्रयास करें।",
|
||||
"aborted": "टाइमआउट के कारण कॉपी ऑपरेशन रद्द कर दिया गया।"
|
||||
}
|
||||
},
|
||||
"invalidDate": "अमान्य दिनांक"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "फाइलें खोजें...",
|
||||
"results": "{total} में से {filtered} फाइलें मिलीं"
|
||||
"results": "{total} में से {filtered} फाइलें मिलीं",
|
||||
"placeholderFolders": "फ़ोल्डर खोजें...",
|
||||
"noResults": "\"{query}\" के लिए कोई परिणाम नहीं मिला",
|
||||
"placeholderFiles": "फाइलें खोजें..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1223,7 +1382,18 @@
|
||||
"editSuccess": "साझाकरण सफलतापूर्वक अपडेट किया गया",
|
||||
"editError": "साझाकरण अपडेट करने में विफल",
|
||||
"bulkDeleteConfirmation": "क्या आप वाकई {count, plural, =1 {1 साझाकरण} other {# साझाकरण}} हटाना चाहते हैं? इस क्रिया को पूर्ववत नहीं किया जा सकता।",
|
||||
"bulkDeleteTitle": "चयनित साझाकरण हटाएं"
|
||||
"bulkDeleteTitle": "चयनित साझाकरण हटाएं",
|
||||
"addDescriptionPlaceholder": "विवरण जोड़ें...",
|
||||
"aliasLabel": "लिंक उपनाम",
|
||||
"aliasPlaceholder": "कस्टम उपनाम दर्ज करें",
|
||||
"copyLink": "लिंक कॉपी करें",
|
||||
"fileTitle": "फ़ाइल साझा करें",
|
||||
"folderTitle": "फ़ोल्डर साझा करें",
|
||||
"generateLink": "लिंक जेनरेट करें",
|
||||
"linkDescriptionFile": "फ़ाइल साझा करने के लिए कस्टम लिंक जेनरेट करें",
|
||||
"linkDescriptionFolder": "फ़ोल्डर साझा करने के लिए कस्टम लिंक जेनरेट करें",
|
||||
"linkReady": "आपका साझाकरण लिंक तैयार है:",
|
||||
"linkTitle": "लिंक जेनरेट करें"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "साझाकरण विवरण",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "फाइलें",
|
||||
"totalSize": "कुल आकार",
|
||||
"creating": "बनाया जा रहा है...",
|
||||
"create": "साझाकरण बनाएं"
|
||||
"create": "साझाकरण बनाएं",
|
||||
"itemsToShare": "साझा करने वाले आइटम ({count} आइटम)"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "इस साझाकरण के लिए पासवर्ड सुरक्षा और सुरक्षा विकल्प कॉन्फ़िगर करें",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "चयनित डाउनलोड करें"
|
||||
},
|
||||
"selectAll": "सभी चुनें",
|
||||
"selectShare": "साझाकरण {shareName} चुनें"
|
||||
"selectShare": "साझाकरण {shareName} चुनें",
|
||||
"folderCount": "फ़ोल्डर"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "स्टोरेज उपयोग",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "Aggiorna",
|
||||
"click": "Clicca per",
|
||||
"creating": "Creazione in corso...",
|
||||
"loadingSimple": "Caricamento..."
|
||||
"loadingSimple": "Caricamento...",
|
||||
"create": "Crea",
|
||||
"deleting": "Eliminazione...",
|
||||
"move": "Sposta",
|
||||
"rename": "Rinomina",
|
||||
"search": "Cerca",
|
||||
"share": "Condividi"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Crea Condivisione",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "Crea Condivisione",
|
||||
"success": "Condivisione creata con successo",
|
||||
"error": "Errore nella creazione della condivisione",
|
||||
"namePlaceholder": "Inserisci un nome per la tua condivisione"
|
||||
"namePlaceholder": "Inserisci un nome per la tua condivisione",
|
||||
"nextSelectFiles": "Avanti: Seleziona file",
|
||||
"searchLabel": "Cerca",
|
||||
"tabs": {
|
||||
"shareDetails": "Dettagli condivisione",
|
||||
"selectFiles": "Seleziona file"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Personalizzazione",
|
||||
"colors": {
|
||||
"title": "Colori del Tema",
|
||||
"description": "Scegli il tuo colore primario preferito per il tema",
|
||||
"presets": "Colori Disponibili",
|
||||
"presetsDescription": "Seleziona tra i temi colore disponibili",
|
||||
"reset": "Ripristina Predefinito"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Tipografia",
|
||||
"description": "Scegli la tua famiglia di caratteri preferita",
|
||||
"available": "Caratteri Disponibili",
|
||||
"availableDescription": "Seleziona tra le famiglie di caratteri disponibili",
|
||||
"reset": "Ripristina Predefinito"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Raggio dei Bordi",
|
||||
"description": "Personalizza la rotondità degli elementi dell'interfaccia",
|
||||
"available": "Opzioni di Rotondità",
|
||||
"availableDescription": "Scegli come dovrebbero apparire gli angoli arrotondati",
|
||||
"reset": "Ripristina Predefinito"
|
||||
},
|
||||
"background": {
|
||||
"title": "Colori di Sfondo",
|
||||
"description": "Personalizza i colori di sfondo per le modalità chiara e scura",
|
||||
"lightMode": "Modalità Chiara",
|
||||
"darkMode": "Modalità Scura",
|
||||
"availableDescription": "Scegli i colori di sfondo per entrambi i temi chiari e scuri",
|
||||
"reset": "Ripristina Predefinito"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Modalità Tema",
|
||||
"description": "Scegli tra tema chiaro, scuro o di sistema",
|
||||
"selectTheme": "Preferenza Tema",
|
||||
"availableDescription": "Seleziona la tua modalità tema preferita",
|
||||
"reset": "Ripristina Sistema"
|
||||
},
|
||||
"pageTitle": "Personalizzazione"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Errore durante il caricamento dei dati del pannello di controllo",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "File da eliminare",
|
||||
"sharesToDelete": "Condivisioni che saranno eliminate"
|
||||
"sharesToDelete": "Condivisioni che saranno eliminate",
|
||||
"foldersToDelete": "Cartelle da eliminare",
|
||||
"itemsToDelete": "Elementi da eliminare"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Download in coda: {fileName}",
|
||||
"queuedDescription": "Il tuo download inizierà automaticamente quando si libera uno slot",
|
||||
"queuePosition": "Download in coda alla posizione {position}: {fileName}",
|
||||
"estimatedWait": "Tempo di attesa stimato: {time}",
|
||||
"queueFull": "Coda di download piena",
|
||||
"queueFullDescription": "Riprova tra qualche minuto quando la coda avrà spazio",
|
||||
"cancelSuccess": "Download annullato con successo",
|
||||
"cancelError": "Impossibile annullare il download: {error}",
|
||||
"status": {
|
||||
"pending": "Preparazione...",
|
||||
"queued": "In coda",
|
||||
"downloading": "Download in corso",
|
||||
"completed": "Completato",
|
||||
"failed": "Fallito"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}s",
|
||||
"minutes": "{minutes}m",
|
||||
"hoursMinutes": "{hours}h {minutes}m"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Download",
|
||||
"downloads": "Coda Download",
|
||||
"active": "Attivi",
|
||||
"queued": "In Coda",
|
||||
"position": "Posizione {position}",
|
||||
"estimatedWait": "Attesa: {time}",
|
||||
"unknownFile": "File sconosciuto",
|
||||
"noDownloads": "Nessun download in corso",
|
||||
"refresh": "Aggiorna Coda"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Nessun file caricato ancora",
|
||||
@@ -201,6 +288,7 @@
|
||||
"extension": "Estensione",
|
||||
"descriptionLabel": "Descrizione",
|
||||
"descriptionPlaceholder": "Inserisci descrizione del file",
|
||||
"addDescriptionPlaceholder": "Aggiungi descrizione...",
|
||||
"deleteFile": "Elimina File",
|
||||
"deleteConfirmation": "Sei sicuro di voler eliminare ?",
|
||||
"deleteWarning": "Questa azione non può essere annullata."
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Anteprima file",
|
||||
"addToShare": "Aggiungi alla condivisione",
|
||||
"removeFromShare": "Rimuovi dalla condivisione",
|
||||
"saveChanges": "Salva Modifiche"
|
||||
"saveChanges": "Salva Modifiche",
|
||||
"editFolder": "Modifica cartella"
|
||||
},
|
||||
"files": {
|
||||
"title": "Tutti i File",
|
||||
@@ -267,11 +356,24 @@
|
||||
"bulkDownloadFileError": "Errore nel download del file {fileName}",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 file eliminato con successo} other {# file eliminati con successo}}",
|
||||
"bulkDeleteError": "Errore nell'eliminazione dei file selezionati",
|
||||
"bulkDeleteTitle": "Elimina File Selezionati",
|
||||
"bulkDeleteConfirmation": "Sei sicuro di voler eliminare {count, plural, =1 {1 file} other {# file}}? Questa azione non può essere annullata.",
|
||||
"viewMode": {
|
||||
"table": "Tabella",
|
||||
"grid": "Griglia"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {Nessun file} =1 {1 file} other {# file}}"
|
||||
"totalFiles": "{count, plural, =0 {Nessun file} =1 {1 file} other {# file}}",
|
||||
"actions": {
|
||||
"open": "Apri",
|
||||
"rename": "Rinomina",
|
||||
"delete": "Elimina"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Ancora nessun file o cartella",
|
||||
"description": "Carica il tuo primo file o crea una cartella per iniziare"
|
||||
},
|
||||
"files": "file",
|
||||
"folders": "cartelle"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Tabella dei file",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Elimina Selezionati"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Modifica cartella",
|
||||
"folderName": "Nome cartella",
|
||||
"folderNamePlaceholder": "Inserisci nome cartella",
|
||||
"folderDescription": "Descrizione",
|
||||
"folderDescriptionPlaceholder": "Inserisci descrizione cartella (opzionale)",
|
||||
"createFolder": "Crea nuova cartella",
|
||||
"renameFolder": "Rinomina cartella",
|
||||
"moveFolder": "Sposta cartella",
|
||||
"shareFolder": "Condividi cartella",
|
||||
"deleteFolder": "Elimina cartella",
|
||||
"moveTo": "Sposta in",
|
||||
"selectDestination": "Seleziona cartella destinazione",
|
||||
"rootFolder": "Radice",
|
||||
"folderCreated": "Cartella creata con successo",
|
||||
"folderRenamed": "Cartella rinominata con successo",
|
||||
"folderMoved": "Cartella spostata con successo",
|
||||
"folderDeleted": "Cartella eliminata con successo",
|
||||
"folderShared": "Cartella condivisa con successo",
|
||||
"createFolderError": "Errore nella creazione della cartella",
|
||||
"renameFolderError": "Errore nella rinominazione della cartella",
|
||||
"moveFolderError": "Errore nello spostamento della cartella",
|
||||
"deleteFolderError": "Errore nell'eliminazione della cartella",
|
||||
"shareFolderError": "Errore nella condivisione della cartella",
|
||||
"deleteConfirmation": "Sei sicuro di voler eliminare questa cartella?",
|
||||
"deleteWarning": "Questa azione non può essere annullata."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Sviluppato da",
|
||||
"kyanHomepage": "Homepage di Kyantech"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Errore durante la rimozione del logo"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Elementi da spostare:",
|
||||
"movingTo": "Spostamento in:",
|
||||
"title": "Sposta {count, plural, =1 {elemento} other {elementi}}",
|
||||
"description": "Sposta {count, plural, =1 {elemento} other {elementi}} in una nuova posizione",
|
||||
"success": "Spostati con successo {count} {count, plural, =1 {elemento} other {elementi}}"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Logo dell'App",
|
||||
"profileMenu": "Menu Profilo",
|
||||
"profile": "Profilo",
|
||||
"settings": "Impostazioni",
|
||||
"usersManagement": "Gestione Utenti",
|
||||
"logout": "Logout"
|
||||
"logout": "Logout",
|
||||
"customization": "Personalizzazione"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Pannello di controllo"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Notifiche download abilitate",
|
||||
"permissionDenied": "Notifiche download disabilitate",
|
||||
"downloadComplete": {
|
||||
"title": "Download Completato",
|
||||
"body": "Il download di {fileName} è terminato"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Download Fallito",
|
||||
"body": "Impossibile scaricare {fileName}: {error}",
|
||||
"unknownError": "Errore sconosciuto"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Download in Avvio",
|
||||
"body": "{fileName} sta ora scaricando{position}",
|
||||
"position": " (era #{position} in coda)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Cambia Parola d'accesso",
|
||||
@@ -750,6 +905,7 @@
|
||||
"noFiles": "Nessun file ricevuto ancora",
|
||||
"noFilesDescription": "I file inviati attraverso questo link appariranno qui",
|
||||
"fileCount": "{count, plural, =0 {Nessun file} =1 {1 file} other {# file}}",
|
||||
"invalidDate": "Data non valida",
|
||||
"totalSize": "Dimensione totale: {size}",
|
||||
"columns": {
|
||||
"file": "File",
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Cerca file...",
|
||||
"results": "Trovati {filtered} di {total} file"
|
||||
"results": "Trovati {filtered} di {total} file",
|
||||
"placeholderFolders": "Cerca cartelle...",
|
||||
"noResults": "Nessun risultato trovato per \"{query}\"",
|
||||
"placeholderFiles": "Cerca file..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1205,6 +1364,7 @@
|
||||
"shareActions": {
|
||||
"deleteTitle": "Elimina Condivisione",
|
||||
"deleteConfirmation": "Sei sicuro di voler eliminare questa condivisione? Questa azione non può essere annullata.",
|
||||
"addDescriptionPlaceholder": "Aggiungi descrizione...",
|
||||
"editTitle": "Modifica Condivisione",
|
||||
"nameLabel": "Nome Condivisione",
|
||||
"descriptionLabel": "Descrizione",
|
||||
@@ -1223,7 +1383,17 @@
|
||||
"editSuccess": "Condivisione aggiornata con successo",
|
||||
"editError": "Errore nell'aggiornamento della condivisione",
|
||||
"bulkDeleteConfirmation": "Sei sicuro di voler eliminare {count, plural, =1 {1 condivisione} other {# condivisioni}}? Questa azione non può essere annullata.",
|
||||
"bulkDeleteTitle": "Elimina Condivisioni Selezionate"
|
||||
"bulkDeleteTitle": "Elimina Condivisioni Selezionate",
|
||||
"aliasLabel": "Alias collegamento",
|
||||
"aliasPlaceholder": "Inserisci alias personalizzato",
|
||||
"copyLink": "Copia collegamento",
|
||||
"fileTitle": "Condividi file",
|
||||
"folderTitle": "Condividi cartella",
|
||||
"generateLink": "Genera collegamento",
|
||||
"linkDescriptionFile": "Genera un collegamento personalizzato per condividere il file",
|
||||
"linkDescriptionFolder": "Genera un collegamento personalizzato per condividere la cartella",
|
||||
"linkReady": "Il tuo collegamento di condivisione è pronto:",
|
||||
"linkTitle": "Genera collegamento"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Dettagli Condivisione",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "file",
|
||||
"totalSize": "Dimensione totale",
|
||||
"creating": "Creazione...",
|
||||
"create": "Crea Condivisione"
|
||||
"create": "Crea Condivisione",
|
||||
"itemsToShare": "Elementi da condividere ({count} {count, plural, =1 {elemento} other {elementi}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "Configura protezione password e opzioni di sicurezza per questa condivisione",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "Scarica selezionato"
|
||||
},
|
||||
"selectAll": "Seleziona tutto",
|
||||
"selectShare": "Seleziona condivisione {shareName}"
|
||||
"selectShare": "Seleziona condivisione {shareName}",
|
||||
"folderCount": "cartelle"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "Utilizzo Archiviazione",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "更新",
|
||||
"click": "クリックして",
|
||||
"creating": "作成中...",
|
||||
"loadingSimple": "読み込み中..."
|
||||
"loadingSimple": "読み込み中...",
|
||||
"create": "作成",
|
||||
"deleting": "削除中...",
|
||||
"move": "移動",
|
||||
"rename": "名前を変更",
|
||||
"search": "検索",
|
||||
"share": "共有"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "共有を作成",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "共有を作成",
|
||||
"success": "共有が正常に作成されました",
|
||||
"error": "共有の作成に失敗しました",
|
||||
"namePlaceholder": "共有の名前を入力してください"
|
||||
"namePlaceholder": "共有の名前を入力してください",
|
||||
"nextSelectFiles": "次へ:ファイルを選択",
|
||||
"searchLabel": "検索",
|
||||
"tabs": {
|
||||
"shareDetails": "共有の詳細",
|
||||
"selectFiles": "ファイルを選択"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "カスタマイズ",
|
||||
"colors": {
|
||||
"title": "テーマカラー",
|
||||
"description": "お好みのプライマリカラーテーマを選択してください",
|
||||
"presets": "利用可能な色",
|
||||
"presetsDescription": "利用可能なカラーテーマから選択",
|
||||
"reset": "デフォルトにリセット"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "タイポグラフィ",
|
||||
"description": "お好みのフォントファミリーを選択してください",
|
||||
"available": "利用可能なフォント",
|
||||
"availableDescription": "利用可能なフォントファミリーから選択",
|
||||
"reset": "デフォルトにリセット"
|
||||
},
|
||||
"radius": {
|
||||
"title": "角丸",
|
||||
"description": "インターフェース要素の丸みをカスタマイズ",
|
||||
"available": "丸みのオプション",
|
||||
"availableDescription": "角の丸みの表示方法を選択",
|
||||
"reset": "デフォルトにリセット"
|
||||
},
|
||||
"background": {
|
||||
"title": "背景色",
|
||||
"description": "ライトモードとダークモードの背景色をカスタマイズ",
|
||||
"lightMode": "ライトモード",
|
||||
"darkMode": "ダークモード",
|
||||
"availableDescription": "ライトテーマとダークテーマの背景色を選択",
|
||||
"reset": "デフォルトにリセット"
|
||||
},
|
||||
"theme": {
|
||||
"title": "テーマモード",
|
||||
"description": "ライト、ダーク、またはシステムテーマから選択",
|
||||
"selectTheme": "テーマ設定",
|
||||
"availableDescription": "お好みのテーマモードを選択",
|
||||
"reset": "システム設定に戻す"
|
||||
},
|
||||
"pageTitle": "カスタマイズ"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "ダッシュボードデータの読み込みに失敗しました",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "削除するファイル",
|
||||
"sharesToDelete": "削除される共有"
|
||||
"sharesToDelete": "削除される共有",
|
||||
"foldersToDelete": "削除するフォルダ",
|
||||
"itemsToDelete": "削除するアイテム"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "ダウンロードキューに追加: {fileName}",
|
||||
"queuedDescription": "空きスロットができ次第、自動的にダウンロードが開始されます",
|
||||
"queuePosition": "順番{position}でキューに追加: {fileName}",
|
||||
"estimatedWait": "推定待ち時間: {time}",
|
||||
"queueFull": "ダウンロードキューが満杯です",
|
||||
"queueFullDescription": "キューに空きができるまで数分お待ちください",
|
||||
"cancelSuccess": "ダウンロードをキャンセルしました",
|
||||
"cancelError": "ダウンロードのキャンセルに失敗: {error}",
|
||||
"status": {
|
||||
"pending": "準備中...",
|
||||
"queued": "キュー待ち",
|
||||
"downloading": "ダウンロード中",
|
||||
"completed": "完了",
|
||||
"failed": "失敗"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}秒",
|
||||
"minutes": "{minutes}分",
|
||||
"hoursMinutes": "{hours}時間{minutes}分"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "ダウンロード",
|
||||
"downloads": "ダウンロードキュー",
|
||||
"active": "実行中",
|
||||
"queued": "待機中",
|
||||
"position": "順番 {position}",
|
||||
"estimatedWait": "待ち時間: {time}",
|
||||
"unknownFile": "不明なファイル",
|
||||
"noDownloads": "進行中のダウンロードはありません",
|
||||
"refresh": "キューを更新"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "まだファイルがアップロードされていません",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "ファイルの説明を入力してください",
|
||||
"deleteFile": "ファイルを削除",
|
||||
"deleteConfirmation": "を削除してもよろしいですか?",
|
||||
"deleteWarning": "この操作は元に戻せません。"
|
||||
"deleteWarning": "この操作は元に戻せません。",
|
||||
"addDescriptionPlaceholder": "説明を追加..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "ファイルのダウンロードに失敗しました",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "ファイルをプレビュー",
|
||||
"addToShare": "共有に追加",
|
||||
"removeFromShare": "共有から削除",
|
||||
"saveChanges": "変更を保存"
|
||||
"saveChanges": "変更を保存",
|
||||
"editFolder": "フォルダを編集"
|
||||
},
|
||||
"files": {
|
||||
"title": "すべてのファイル",
|
||||
@@ -271,7 +360,20 @@
|
||||
"table": "テーブル",
|
||||
"grid": "グリッド"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {ファイルなし} =1 {1ファイル} other {#ファイル}}"
|
||||
"totalFiles": "{count, plural, =0 {ファイルなし} =1 {1ファイル} other {#ファイル}}",
|
||||
"bulkDeleteConfirmation": "{count, plural, =1 {1つのファイル} other {#つのファイル}}を削除してよろしいですか?この操作は元に戻せません。",
|
||||
"bulkDeleteTitle": "選択したファイルを削除",
|
||||
"actions": {
|
||||
"open": "開く",
|
||||
"rename": "名前を変更",
|
||||
"delete": "削除"
|
||||
},
|
||||
"empty": {
|
||||
"title": "まだファイルやフォルダがありません",
|
||||
"description": "最初のファイルをアップロードするか、フォルダを作成して始めましょう"
|
||||
},
|
||||
"files": "ファイル",
|
||||
"folders": "フォルダ"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "ファイルテーブル",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "選択済みを削除"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "フォルダを編集",
|
||||
"folderName": "フォルダ名",
|
||||
"folderNamePlaceholder": "フォルダ名を入力",
|
||||
"folderDescription": "説明",
|
||||
"folderDescriptionPlaceholder": "フォルダの説明を入力(任意)",
|
||||
"createFolder": "新しいフォルダを作成",
|
||||
"renameFolder": "フォルダ名を変更",
|
||||
"moveFolder": "フォルダを移動",
|
||||
"shareFolder": "フォルダを共有",
|
||||
"deleteFolder": "フォルダを削除",
|
||||
"moveTo": "移動先",
|
||||
"selectDestination": "移動先フォルダを選択",
|
||||
"rootFolder": "ルート",
|
||||
"folderCreated": "フォルダが正常に作成されました",
|
||||
"folderRenamed": "フォルダが正常に名前変更されました",
|
||||
"folderMoved": "フォルダが正常に移動されました",
|
||||
"folderDeleted": "フォルダが正常に削除されました",
|
||||
"folderShared": "フォルダが正常に共有されました",
|
||||
"createFolderError": "フォルダの作成中にエラーが発生しました",
|
||||
"renameFolderError": "フォルダの名前変更中にエラーが発生しました",
|
||||
"moveFolderError": "フォルダの移動中にエラーが発生しました",
|
||||
"deleteFolderError": "フォルダの削除中にエラーが発生しました",
|
||||
"shareFolderError": "フォルダの共有中にエラーが発生しました",
|
||||
"deleteConfirmation": "このフォルダを削除してもよろしいですか?",
|
||||
"deleteWarning": "この操作は元に戻すことができません。"
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "提供:",
|
||||
"kyanHomepage": "Kyantech ホームページ"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "ロゴの削除に失敗しました"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "移動するアイテム:",
|
||||
"movingTo": "移動先:",
|
||||
"title": "アイテムを移動",
|
||||
"description": "アイテムを新しい場所に移動",
|
||||
"success": "{count}個のアイテムが正常に移動されました"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "アプリケーションロゴ",
|
||||
"profileMenu": "プロフィールメニュー",
|
||||
"profile": "プロフィール",
|
||||
"settings": "設定",
|
||||
"usersManagement": "ユーザー管理",
|
||||
"logout": "ログアウト"
|
||||
"logout": "ログアウト",
|
||||
"customization": "カスタマイズ"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "ダッシュボード"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "ダウンロード通知が有効になりました",
|
||||
"permissionDenied": "ダウンロード通知が無効になりました",
|
||||
"downloadComplete": {
|
||||
"title": "ダウンロード完了",
|
||||
"body": "{fileName}のダウンロードが完了しました"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "ダウンロード失敗",
|
||||
"body": "{fileName}のダウンロードに失敗: {error}",
|
||||
"unknownError": "不明なエラー"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "ダウンロード開始",
|
||||
"body": "{fileName}のダウンロードを開始しています{position}",
|
||||
"position": "(キュー内{position}番目)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "パスワードを変更",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "コピー操作がタイムアウトしました。より小さいファイルで再試行するか、接続を確認してください。",
|
||||
"failed": "コピー操作に失敗しました。もう一度お試しください。",
|
||||
"aborted": "タイムアウトによりコピー操作がキャンセルされました。"
|
||||
}
|
||||
},
|
||||
"invalidDate": "無効な日付"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "ファイルを検索...",
|
||||
"results": "全{total}件中{filtered}件が見つかりました"
|
||||
"results": "全{total}件中{filtered}件が見つかりました",
|
||||
"placeholderFolders": "フォルダを検索...",
|
||||
"noResults": "\"{query}\"の検索結果が見つかりませんでした",
|
||||
"placeholderFiles": "ファイルを検索..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1223,7 +1382,18 @@
|
||||
"editSuccess": "共有が正常に更新されました",
|
||||
"editError": "共有の更新に失敗しました",
|
||||
"bulkDeleteConfirmation": "{count, plural, =1 {1つの共有} other {#つの共有}}を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
"bulkDeleteTitle": "選択した共有を削除"
|
||||
"bulkDeleteTitle": "選択した共有を削除",
|
||||
"addDescriptionPlaceholder": "説明を追加...",
|
||||
"aliasLabel": "リンクエイリアス",
|
||||
"aliasPlaceholder": "カスタムエイリアスを入力",
|
||||
"copyLink": "リンクをコピー",
|
||||
"fileTitle": "ファイルを共有",
|
||||
"folderTitle": "フォルダを共有",
|
||||
"generateLink": "リンクを生成",
|
||||
"linkDescriptionFile": "ファイルを共有するためのカスタムリンクを生成",
|
||||
"linkDescriptionFolder": "フォルダを共有するためのカスタムリンクを生成",
|
||||
"linkReady": "共有リンクの準備ができました:",
|
||||
"linkTitle": "リンクを生成"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "共有詳細",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "ファイル",
|
||||
"totalSize": "合計サイズ",
|
||||
"creating": "作成中...",
|
||||
"create": "共有を作成"
|
||||
"create": "共有を作成",
|
||||
"itemsToShare": "共有するアイテム({count}個のアイテム)"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "この共有のパスワード保護とセキュリティオプションを設定",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "選択したダウンロード"
|
||||
},
|
||||
"selectAll": "すべて選択",
|
||||
"selectShare": "共有{shareName}を選択"
|
||||
"selectShare": "共有{shareName}を選択",
|
||||
"folderCount": "フォルダ"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "ストレージ使用量",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "업데이트",
|
||||
"click": "클릭하여",
|
||||
"creating": "생성 중...",
|
||||
"loadingSimple": "로딩 중..."
|
||||
"loadingSimple": "로딩 중...",
|
||||
"create": "생성",
|
||||
"deleting": "삭제 중...",
|
||||
"move": "이동",
|
||||
"rename": "이름 변경",
|
||||
"search": "검색",
|
||||
"share": "공유"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "공유 생성",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "공유 생성",
|
||||
"success": "공유가 성공적으로 생성되었습니다",
|
||||
"error": "공유 생성에 실패했습니다",
|
||||
"namePlaceholder": "공유 이름을 입력하세요"
|
||||
"namePlaceholder": "공유 이름을 입력하세요",
|
||||
"nextSelectFiles": "다음: 파일 선택",
|
||||
"searchLabel": "검색",
|
||||
"tabs": {
|
||||
"shareDetails": "공유 세부사항",
|
||||
"selectFiles": "파일 선택"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "커스터마이징",
|
||||
"colors": {
|
||||
"title": "테마 색상",
|
||||
"description": "선호하는 기본 색상 테마를 선택하세요",
|
||||
"presets": "사용 가능한 색상",
|
||||
"presetsDescription": "사용 가능한 색상 테마에서 선택하세요",
|
||||
"reset": "기본값으로 재설정"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "타이포그래피",
|
||||
"description": "선호하는 글꼴을 선택하세요",
|
||||
"available": "사용 가능한 글꼴",
|
||||
"availableDescription": "사용 가능한 글꼴 중에서 선택하세요",
|
||||
"reset": "기본값으로 재설정"
|
||||
},
|
||||
"radius": {
|
||||
"title": "테두리 반경",
|
||||
"description": "인터페이스 요소의 둥근 정도를 조정하세요",
|
||||
"available": "둥근 정도 옵션",
|
||||
"availableDescription": "모서리의 둥근 정도를 선택하세요",
|
||||
"reset": "기본값으로 재설정"
|
||||
},
|
||||
"background": {
|
||||
"title": "배경 색상",
|
||||
"description": "라이트 모드와 다크 모드의 배경 색상을 조정하세요",
|
||||
"lightMode": "라이트 모드",
|
||||
"darkMode": "다크 모드",
|
||||
"availableDescription": "라이트와 다크 테마의 배경 색상을 선택하세요",
|
||||
"reset": "기본값으로 재설정"
|
||||
},
|
||||
"theme": {
|
||||
"title": "테마 모드",
|
||||
"description": "라이트, 다크 또는 시스템 테마 중 선택하세요",
|
||||
"selectTheme": "테마 설정",
|
||||
"availableDescription": "선호하는 테마 모드를 선택하세요",
|
||||
"reset": "시스템 설정으로 재설정"
|
||||
},
|
||||
"pageTitle": "커스터마이징"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "대시보드 데이터를 불러오는데 실패했습니다",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "삭제할 파일",
|
||||
"sharesToDelete": "삭제될 공유"
|
||||
"sharesToDelete": "삭제될 공유",
|
||||
"foldersToDelete": "삭제할 폴더",
|
||||
"itemsToDelete": "삭제할 항목"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "다운로드 대기 중: {fileName}",
|
||||
"queuedDescription": "슬롯이 사용 가능해지면 자동으로 다운로드가 시작됩니다",
|
||||
"queuePosition": "다운로드 대기 순서 {position}번: {fileName}",
|
||||
"estimatedWait": "예상 대기 시간: {time}",
|
||||
"queueFull": "다운로드 대기열이 가득 찼습니다",
|
||||
"queueFullDescription": "대기열에 여유가 생길 때까지 잠시 후 다시 시도해 주세요",
|
||||
"cancelSuccess": "다운로드가 성공적으로 취소되었습니다",
|
||||
"cancelError": "다운로드 취소 실패: {error}",
|
||||
"status": {
|
||||
"pending": "준비 중...",
|
||||
"queued": "대기 중",
|
||||
"downloading": "다운로드 중",
|
||||
"completed": "완료됨",
|
||||
"failed": "실패"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}초",
|
||||
"minutes": "{minutes}분",
|
||||
"hoursMinutes": "{hours}시간 {minutes}분"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "다운로드",
|
||||
"downloads": "다운로드 대기열",
|
||||
"active": "진행 중",
|
||||
"queued": "대기 중",
|
||||
"position": "순서 {position}",
|
||||
"estimatedWait": "대기: {time}",
|
||||
"unknownFile": "알 수 없는 파일",
|
||||
"noDownloads": "진행 중인 다운로드 없음",
|
||||
"refresh": "대기열 새로고침"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "아직 파일이 업로드되지 않았습니다",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "파일 설명을 입력하세요",
|
||||
"deleteFile": "파일 삭제",
|
||||
"deleteConfirmation": "을(를) 삭제하시겠습니까?",
|
||||
"deleteWarning": "이 작업은 취소할 수 없습니다."
|
||||
"deleteWarning": "이 작업은 취소할 수 없습니다.",
|
||||
"addDescriptionPlaceholder": "설명 추가..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "파일 다운로드에 실패했습니다",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "파일 미리보기",
|
||||
"addToShare": "공유에 추가",
|
||||
"removeFromShare": "공유에서 제거",
|
||||
"saveChanges": "변경사항 저장"
|
||||
"saveChanges": "변경사항 저장",
|
||||
"editFolder": "폴더 편집"
|
||||
},
|
||||
"files": {
|
||||
"title": "모든 파일",
|
||||
@@ -271,7 +360,20 @@
|
||||
"table": "테이블",
|
||||
"grid": "그리드"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {파일 없음} =1 {1개 파일} other {#개 파일}}"
|
||||
"totalFiles": "{count, plural, =0 {파일 없음} =1 {1개 파일} other {#개 파일}}",
|
||||
"bulkDeleteConfirmation": "{count, plural, =1 {1개 파일} other {#개 파일}}을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
||||
"bulkDeleteTitle": "선택한 파일 삭제",
|
||||
"actions": {
|
||||
"open": "열기",
|
||||
"rename": "이름 변경",
|
||||
"delete": "삭제"
|
||||
},
|
||||
"empty": {
|
||||
"title": "아직 파일이나 폴더가 없습니다",
|
||||
"description": "첫 번째 파일을 업로드하거나 폴더를 만들어 시작하세요"
|
||||
},
|
||||
"files": "파일",
|
||||
"folders": "폴더"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "파일 테이블",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "선택된 항목 삭제"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "폴더 편집",
|
||||
"folderName": "폴더 이름",
|
||||
"folderNamePlaceholder": "폴더 이름 입력",
|
||||
"folderDescription": "설명",
|
||||
"folderDescriptionPlaceholder": "폴더 설명 입력 (선택사항)",
|
||||
"createFolder": "새 폴더 만들기",
|
||||
"renameFolder": "폴더 이름 변경",
|
||||
"moveFolder": "폴더 이동",
|
||||
"shareFolder": "폴더 공유",
|
||||
"deleteFolder": "폴더 삭제",
|
||||
"moveTo": "이동 위치",
|
||||
"selectDestination": "대상 폴더 선택",
|
||||
"rootFolder": "루트",
|
||||
"folderCreated": "폴더가 성공적으로 생성되었습니다",
|
||||
"folderRenamed": "폴더 이름이 성공적으로 변경되었습니다",
|
||||
"folderMoved": "폴더가 성공적으로 이동되었습니다",
|
||||
"folderDeleted": "폴더가 성공적으로 삭제되었습니다",
|
||||
"folderShared": "폴더가 성공적으로 공유되었습니다",
|
||||
"createFolderError": "폴더 생성 중 오류 발생",
|
||||
"renameFolderError": "폴더 이름 변경 중 오류 발생",
|
||||
"moveFolderError": "폴더 이동 중 오류 발생",
|
||||
"deleteFolderError": "폴더 삭제 중 오류 발생",
|
||||
"shareFolderError": "폴더 공유 중 오류 발생",
|
||||
"deleteConfirmation": "이 폴더를 삭제하시겠습니까?",
|
||||
"deleteWarning": "이 작업은 되돌릴 수 없습니다."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "제공:",
|
||||
"kyanHomepage": "Kyantech 홈페이지"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "로고 삭제에 실패했습니다"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "이동할 항목:",
|
||||
"movingTo": "이동 위치:",
|
||||
"title": "항목 이동",
|
||||
"description": "항목을 새 위치로 이동",
|
||||
"success": "{count}개 항목이 성공적으로 이동되었습니다"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "애플리케이션 로고",
|
||||
"profileMenu": "프로필 메뉴",
|
||||
"profile": "프로필",
|
||||
"settings": "설정",
|
||||
"usersManagement": "사용자 관리",
|
||||
"logout": "로그아웃"
|
||||
"logout": "로그아웃",
|
||||
"customization": "사용자 정의"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "대시보드"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "다운로드 알림이 활성화되었습니다",
|
||||
"permissionDenied": "다운로드 알림이 비활성화되었습니다",
|
||||
"downloadComplete": {
|
||||
"title": "다운로드 완료",
|
||||
"body": "{fileName} 다운로드가 완료되었습니다"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "다운로드 실패",
|
||||
"body": "{fileName} 다운로드 실패: {error}",
|
||||
"unknownError": "알 수 없는 오류"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "다운로드 시작",
|
||||
"body": "{fileName} 다운로드가 시작되었습니다{position}",
|
||||
"position": " (대기열 #{position}번이었음)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "비밀번호 변경",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "복사 작업 시간이 초과되었습니다. 더 작은 파일로 다시 시도하거나 연결을 확인하십시오.",
|
||||
"failed": "복사 작업이 실패했습니다. 다시 시도해 주세요.",
|
||||
"aborted": "시간 초과로 인해 복사 작업이 취소되었습니다."
|
||||
}
|
||||
},
|
||||
"invalidDate": "잘못된 날짜"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "파일 검색...",
|
||||
"results": "전체 {total}개 중 {filtered}개 파일을 찾았습니다"
|
||||
"results": "전체 {total}개 중 {filtered}개 파일을 찾았습니다",
|
||||
"placeholderFolders": "폴더 검색...",
|
||||
"noResults": "\"{query}\"에 대한 검색 결과가 없습니다",
|
||||
"placeholderFiles": "파일 검색..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1223,7 +1382,18 @@
|
||||
"editSuccess": "공유가 성공적으로 업데이트되었습니다",
|
||||
"editError": "공유 업데이트에 실패했습니다",
|
||||
"bulkDeleteConfirmation": "{count, plural, =1 {1개의 공유} other {#개의 공유}}를 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
||||
"bulkDeleteTitle": "선택한 공유 삭제"
|
||||
"bulkDeleteTitle": "선택한 공유 삭제",
|
||||
"addDescriptionPlaceholder": "설명 추가...",
|
||||
"aliasLabel": "링크 별칭",
|
||||
"aliasPlaceholder": "사용자 정의 별칭 입력",
|
||||
"copyLink": "링크 복사",
|
||||
"fileTitle": "파일 공유",
|
||||
"folderTitle": "폴더 공유",
|
||||
"generateLink": "링크 생성",
|
||||
"linkDescriptionFile": "파일을 공유할 사용자 정의 링크 생성",
|
||||
"linkDescriptionFolder": "폴더를 공유할 사용자 정의 링크 생성",
|
||||
"linkReady": "공유 링크가 준비되었습니다:",
|
||||
"linkTitle": "링크 생성"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "공유 세부 정보",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "파일",
|
||||
"totalSize": "전체 크기",
|
||||
"creating": "생성 중...",
|
||||
"create": "공유 생성"
|
||||
"create": "공유 생성",
|
||||
"itemsToShare": "공유할 항목 ({count}개 항목)"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "이 공유의 비밀번호 보호 및 보안 옵션을 구성하세요",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "선택한 다운로드"
|
||||
},
|
||||
"selectAll": "모두 선택",
|
||||
"selectShare": "공유 {shareName} 선택"
|
||||
"selectShare": "공유 {shareName} 선택",
|
||||
"folderCount": "폴더"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "스토리지 사용량",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "Bijwerken",
|
||||
"click": "Klik om",
|
||||
"creating": "Maken...",
|
||||
"loadingSimple": "Laden..."
|
||||
"loadingSimple": "Laden...",
|
||||
"create": "Aanmaken",
|
||||
"deleting": "Verwijderen...",
|
||||
"move": "Verplaatsen",
|
||||
"rename": "Hernoemen",
|
||||
"search": "Zoeken",
|
||||
"share": "Delen"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Delen Maken",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "Delen Maken",
|
||||
"success": "Delen succesvol aangemaakt",
|
||||
"error": "Fout bij het aanmaken van delen",
|
||||
"namePlaceholder": "Voer een naam in voor uw delen"
|
||||
"namePlaceholder": "Voer een naam in voor uw delen",
|
||||
"nextSelectFiles": "Volgende: Bestanden selecteren",
|
||||
"searchLabel": "Zoeken",
|
||||
"tabs": {
|
||||
"shareDetails": "Deel details",
|
||||
"selectFiles": "Bestanden selecteren"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Aanpassing",
|
||||
"colors": {
|
||||
"title": "Thema Kleuren",
|
||||
"description": "Kies je gewenste primaire kleurenthema",
|
||||
"presets": "Beschikbare Kleuren",
|
||||
"presetsDescription": "Selecteer uit beschikbare kleurenthema's",
|
||||
"reset": "Standaardinstellingen Herstellen"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Typografie",
|
||||
"description": "Kies je gewenste lettertype",
|
||||
"available": "Beschikbare Lettertypen",
|
||||
"availableDescription": "Selecteer uit beschikbare lettertypen",
|
||||
"reset": "Standaardinstellingen Herstellen"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Randradius",
|
||||
"description": "Pas de rondheid van interface-elementen aan",
|
||||
"available": "Rondheidsopties",
|
||||
"availableDescription": "Kies hoe afgerond hoeken moeten verschijnen",
|
||||
"reset": "Standaardinstellingen Herstellen"
|
||||
},
|
||||
"background": {
|
||||
"title": "Achtergrondkleuren",
|
||||
"description": "Pas achtergrondkleuren aan voor lichte en donkere modi",
|
||||
"lightMode": "Lichte Modus",
|
||||
"darkMode": "Donkere Modus",
|
||||
"availableDescription": "Kies achtergrondkleuren voor zowel lichte als donkere thema's",
|
||||
"reset": "Standaardinstellingen Herstellen"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Thema Modus",
|
||||
"description": "Kies tussen licht, donker of systeemthema",
|
||||
"selectTheme": "Thema Voorkeur",
|
||||
"availableDescription": "Selecteer je gewenste thema modus",
|
||||
"reset": "Terugzetten naar Systeem"
|
||||
},
|
||||
"pageTitle": "Aanpassing"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Fout bij het laden van controlepaneel gegevens",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Te verwijderen bestanden",
|
||||
"sharesToDelete": "Delen die worden verwijderd"
|
||||
"sharesToDelete": "Delen die worden verwijderd",
|
||||
"foldersToDelete": "Mappen die worden verwijderd",
|
||||
"itemsToDelete": "Items die worden verwijderd"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Download in wachtrij: {fileName}",
|
||||
"queuedDescription": "Je download start automatisch wanneer er een slot beschikbaar komt",
|
||||
"queuePosition": "Download in wachtrij op positie {position}: {fileName}",
|
||||
"estimatedWait": "Geschatte wachttijd: {time}",
|
||||
"queueFull": "Downloadwachtrij is vol",
|
||||
"queueFullDescription": "Probeer het over enkele minuten opnieuw wanneer er ruimte is in de wachtrij",
|
||||
"cancelSuccess": "Download succesvol geannuleerd",
|
||||
"cancelError": "Download annuleren mislukt: {error}",
|
||||
"status": {
|
||||
"pending": "Voorbereiden...",
|
||||
"queued": "In wachtrij",
|
||||
"downloading": "Downloaden",
|
||||
"completed": "Voltooid",
|
||||
"failed": "Mislukt"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}s",
|
||||
"minutes": "{minutes}m",
|
||||
"hoursMinutes": "{hours}u {minutes}m"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Downloads",
|
||||
"downloads": "Download Wachtrij",
|
||||
"active": "Actief",
|
||||
"queued": "In Wachtrij",
|
||||
"position": "Positie {position}",
|
||||
"estimatedWait": "Wachttijd: {time}",
|
||||
"unknownFile": "Onbekend bestand",
|
||||
"noDownloads": "Geen downloads actief",
|
||||
"refresh": "Wachtrij Vernieuwen"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Nog geen bestanden geüpload",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "Voer bestandsbeschrijving in",
|
||||
"deleteFile": "Bestand Verwijderen",
|
||||
"deleteConfirmation": "Weet je zeker dat je wilt verwijderen?",
|
||||
"deleteWarning": "Deze actie kan niet ongedaan worden gemaakt."
|
||||
"deleteWarning": "Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"addDescriptionPlaceholder": "Beschrijving toevoegen..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "Fout bij het downloaden van bestand",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Bestand bekijken",
|
||||
"addToShare": "Toevoegen aan share",
|
||||
"removeFromShare": "Verwijderen uit share",
|
||||
"saveChanges": "Wijzigingen Opslaan"
|
||||
"saveChanges": "Wijzigingen Opslaan",
|
||||
"editFolder": "Map bewerken"
|
||||
},
|
||||
"files": {
|
||||
"title": "Alle Bestanden",
|
||||
@@ -271,7 +360,20 @@
|
||||
"table": "Tabel",
|
||||
"grid": "Raster"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {Geen bestanden} =1 {1 bestand} other {# bestanden}}"
|
||||
"totalFiles": "{count, plural, =0 {Geen bestanden} =1 {1 bestand} other {# bestanden}}",
|
||||
"bulkDeleteConfirmation": "Weet je zeker dat je {count, plural, =1 {1 bestand} other {# bestanden}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"bulkDeleteTitle": "Geselecteerde Bestanden Verwijderen",
|
||||
"actions": {
|
||||
"open": "Openen",
|
||||
"rename": "Hernoemen",
|
||||
"delete": "Verwijderen"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nog geen bestanden of mappen",
|
||||
"description": "Upload uw eerste bestand of maak een map om te beginnen"
|
||||
},
|
||||
"files": "bestanden",
|
||||
"folders": "mappen"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Bestanden tabel",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Geselecteerde Verwijderen"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Map bewerken",
|
||||
"folderName": "Mapnaam",
|
||||
"folderNamePlaceholder": "Voer mapnaam in",
|
||||
"folderDescription": "Beschrijving",
|
||||
"folderDescriptionPlaceholder": "Voer mapbeschrijving in (optioneel)",
|
||||
"createFolder": "Nieuwe map maken",
|
||||
"renameFolder": "Map hernoemen",
|
||||
"moveFolder": "Map verplaatsen",
|
||||
"shareFolder": "Map delen",
|
||||
"deleteFolder": "Map verwijderen",
|
||||
"moveTo": "Verplaatsen naar",
|
||||
"selectDestination": "Bestemmingsmap selecteren",
|
||||
"rootFolder": "Hoofdmap",
|
||||
"folderCreated": "Map succesvol aangemaakt",
|
||||
"folderRenamed": "Map succesvol hernoemd",
|
||||
"folderMoved": "Map succesvol verplaatst",
|
||||
"folderDeleted": "Map succesvol verwijderd",
|
||||
"folderShared": "Map succesvol gedeeld",
|
||||
"createFolderError": "Fout bij maken van map",
|
||||
"renameFolderError": "Fout bij hernoemen van map",
|
||||
"moveFolderError": "Fout bij verplaatsen van map",
|
||||
"deleteFolderError": "Fout bij verwijderen van map",
|
||||
"shareFolderError": "Fout bij delen van map",
|
||||
"deleteConfirmation": "Weet u zeker dat u deze map wilt verwijderen?",
|
||||
"deleteWarning": "Deze actie kan niet ongedaan worden gemaakt."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Mogelijk gemaakt door",
|
||||
"kyanHomepage": "Kyantech homepage"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Fout bij het verwijderen van logo"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Items om te verplaatsen:",
|
||||
"movingTo": "Verplaatsen naar:",
|
||||
"title": "{count, plural, =1 {Item} other {Items}} verplaatsen",
|
||||
"description": "{count, plural, =1 {Item} other {Items}} naar een nieuwe locatie verplaatsen",
|
||||
"success": "{count} {count, plural, =1 {item} other {items}} succesvol verplaatst"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Applicatie Logo",
|
||||
"profileMenu": "Profiel Menu",
|
||||
"profile": "Profiel",
|
||||
"settings": "Instellingen",
|
||||
"usersManagement": "Gebruikersbeheer",
|
||||
"logout": "Uitloggen"
|
||||
"logout": "Uitloggen",
|
||||
"customization": "Aanpassen"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Controlepaneel"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Download meldingen ingeschakeld",
|
||||
"permissionDenied": "Download meldingen uitgeschakeld",
|
||||
"downloadComplete": {
|
||||
"title": "Download Voltooid",
|
||||
"body": "{fileName} is klaar met downloaden"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Download Mislukt",
|
||||
"body": "Downloaden van {fileName} mislukt: {error}",
|
||||
"unknownError": "Onbekende fout"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Download Start",
|
||||
"body": "{fileName} wordt nu gedownload{position}",
|
||||
"position": " (was #{position} in wachtrij)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Wachtwoord Wijzigen",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "Kopieeroperatie verlopen. Probeer het opnieuw met een kleiner bestand of controleer uw verbinding.",
|
||||
"failed": "Kopieeroperatie mislukt. Probeer het opnieuw.",
|
||||
"aborted": "Kopieeroperatie is geannuleerd vanwege een time-out."
|
||||
}
|
||||
},
|
||||
"invalidDate": "Ongeldige datum"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Bestanden zoeken...",
|
||||
"results": "{filtered} van {total} bestanden gevonden"
|
||||
"results": "{filtered} van {total} bestanden gevonden",
|
||||
"placeholderFolders": "Zoek mappen...",
|
||||
"noResults": "Geen resultaten gevonden voor \"{query}\"",
|
||||
"placeholderFiles": "Bestanden zoeken..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1223,7 +1382,18 @@
|
||||
"editSuccess": "Delen succesvol bijgewerkt",
|
||||
"editError": "Fout bij bijwerken van delen",
|
||||
"bulkDeleteConfirmation": "Weet je zeker dat je {count, plural, =1 {1 deel} other {# delen}} wilt verwijderen? Deze actie kan niet ongedaan worden gemaakt.",
|
||||
"bulkDeleteTitle": "Geselecteerde Delen Verwijderen"
|
||||
"bulkDeleteTitle": "Geselecteerde Delen Verwijderen",
|
||||
"addDescriptionPlaceholder": "Beschrijving toevoegen...",
|
||||
"aliasLabel": "Link alias",
|
||||
"aliasPlaceholder": "Voer aangepaste alias in",
|
||||
"copyLink": "Link kopiëren",
|
||||
"fileTitle": "Bestand delen",
|
||||
"folderTitle": "Map delen",
|
||||
"generateLink": "Link genereren",
|
||||
"linkDescriptionFile": "Genereer een aangepaste link om het bestand te delen",
|
||||
"linkDescriptionFolder": "Genereer een aangepaste link om de map te delen",
|
||||
"linkReady": "Uw deel-link is klaar:",
|
||||
"linkTitle": "Link genereren"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Delen Details",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "bestanden",
|
||||
"totalSize": "Totale grootte",
|
||||
"creating": "Aanmaken...",
|
||||
"create": "Delen Maken"
|
||||
"create": "Delen Maken",
|
||||
"itemsToShare": "Items om te delen ({count} {count, plural, =1 {item} other {items}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "Configureer wachtwoordbeveiliging en beveiligingsopties voor dit delen",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "Download geselecteerd"
|
||||
},
|
||||
"selectAll": "Alles selecteren",
|
||||
"selectShare": "Deel {shareName} selecteren"
|
||||
"selectShare": "Deel {shareName} selecteren",
|
||||
"folderCount": "mappen"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "Opslaggebruik",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"back": "Wróć",
|
||||
"click": "Kliknij, aby",
|
||||
"creating": "Tworzenie...",
|
||||
"loadingSimple": "Ładowanie..."
|
||||
"loadingSimple": "Ładowanie...",
|
||||
"create": "Utwórz",
|
||||
"deleting": "Usuwanie...",
|
||||
"move": "Przenieś",
|
||||
"rename": "Zmień nazwę",
|
||||
"search": "Szukaj",
|
||||
"share": "Udostępnij"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Utwórz Udostępnienie",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "Utwórz Udostępnienie",
|
||||
"success": "Udostępnienie utworzone pomyślnie",
|
||||
"error": "Nie udało się utworzyć udostępnienia",
|
||||
"namePlaceholder": "Wprowadź nazwę dla swojego udostępnienia"
|
||||
"namePlaceholder": "Wprowadź nazwę dla swojego udostępnienia",
|
||||
"nextSelectFiles": "Dalej: Wybierz pliki",
|
||||
"searchLabel": "Szukaj",
|
||||
"tabs": {
|
||||
"shareDetails": "Szczegóły udostępniania",
|
||||
"selectFiles": "Wybierz pliki"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Personalizacja",
|
||||
"colors": {
|
||||
"title": "Kolory motywu",
|
||||
"description": "Wybierz preferowany kolor podstawowy motywu",
|
||||
"presets": "Dostępne kolory",
|
||||
"presetsDescription": "Wybierz spośród dostępnych motywów kolorystycznych",
|
||||
"reset": "Przywróć domyślne"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Typografia",
|
||||
"description": "Wybierz preferowaną rodzinę czcionek",
|
||||
"available": "Dostępne czcionki",
|
||||
"availableDescription": "Wybierz spośród dostępnych rodzin czcionek",
|
||||
"reset": "Przywróć domyślne"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Zaokrąglenie krawędzi",
|
||||
"description": "Dostosuj zaokrąglenie elementów interfejsu",
|
||||
"available": "Opcje zaokrąglenia",
|
||||
"availableDescription": "Wybierz stopień zaokrąglenia narożników",
|
||||
"reset": "Przywróć domyślne"
|
||||
},
|
||||
"background": {
|
||||
"title": "Kolory tła",
|
||||
"description": "Dostosuj kolory tła dla trybów jasnego i ciemnego",
|
||||
"lightMode": "Tryb jasny",
|
||||
"darkMode": "Tryb ciemny",
|
||||
"availableDescription": "Wybierz kolory tła dla motywów jasnego i ciemnego",
|
||||
"reset": "Przywróć domyślne"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Tryb motywu",
|
||||
"description": "Wybierz między trybem jasnym, ciemnym lub systemowym",
|
||||
"selectTheme": "Preferencje motywu",
|
||||
"availableDescription": "Wybierz preferowany tryb motywu",
|
||||
"reset": "Przywróć ustawienia systemowe"
|
||||
},
|
||||
"pageTitle": "Personalizacja"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Nie udało się załadować danych panelu głównego",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Pliki do usunięcia",
|
||||
"sharesToDelete": "Udostępnienia do usunięcia"
|
||||
"sharesToDelete": "Udostępnienia do usunięcia",
|
||||
"foldersToDelete": "Foldery do usunięcia",
|
||||
"itemsToDelete": "Elementy do usunięcia"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Pobieranie w kolejce: {fileName}",
|
||||
"queuedDescription": "Pobieranie rozpocznie się automatycznie, gdy zwolni się miejsce",
|
||||
"queuePosition": "Pobieranie w kolejce na pozycji {position}: {fileName}",
|
||||
"estimatedWait": "Szacowany czas oczekiwania: {time}",
|
||||
"queueFull": "Kolejka pobierania jest pełna",
|
||||
"queueFullDescription": "Spróbuj ponownie za kilka minut, gdy w kolejce będzie miejsce",
|
||||
"cancelSuccess": "Pobieranie anulowane pomyślnie",
|
||||
"cancelError": "Nie udało się anulować pobierania: {error}",
|
||||
"status": {
|
||||
"pending": "Przygotowywanie...",
|
||||
"queued": "W kolejce",
|
||||
"downloading": "Pobieranie",
|
||||
"completed": "Zakończone",
|
||||
"failed": "Nie powiodło się"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}s",
|
||||
"minutes": "{minutes}m",
|
||||
"hoursMinutes": "{hours}g {minutes}m"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Pobierania",
|
||||
"downloads": "Kolejka pobierania",
|
||||
"active": "Aktywne",
|
||||
"queued": "W kolejce",
|
||||
"position": "Pozycja {position}",
|
||||
"estimatedWait": "Oczekiwanie: {time}",
|
||||
"unknownFile": "Nieznany plik",
|
||||
"noDownloads": "Brak aktywnych pobierań",
|
||||
"refresh": "Odśwież kolejkę"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Brak przesłanych plików",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "Wprowadź opis pliku",
|
||||
"deleteFile": "Usuń plik",
|
||||
"deleteConfirmation": "Czy na pewno chcesz usunąć ten plik?",
|
||||
"deleteWarning": "Tej operacji nie można cofnąć."
|
||||
"deleteWarning": "Tej operacji nie można cofnąć.",
|
||||
"addDescriptionPlaceholder": "Dodaj opis..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "Nie udało się pobrać pliku",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Podgląd pliku",
|
||||
"addToShare": "Dodaj do udostępnienia",
|
||||
"removeFromShare": "Usuń z udostępnienia",
|
||||
"saveChanges": "Zapisz zmiany"
|
||||
"saveChanges": "Zapisz zmiany",
|
||||
"editFolder": "Edytuj folder"
|
||||
},
|
||||
"files": {
|
||||
"title": "Wszystkie pliki",
|
||||
@@ -271,7 +360,20 @@
|
||||
"viewMode": {
|
||||
"table": "Tabela",
|
||||
"grid": "Siatka"
|
||||
}
|
||||
},
|
||||
"bulkDeleteConfirmation": "Czy na pewno chcesz usunąć {count, plural, =1 {1 plik} other {# plików}}? Ta akcja nie może zostać cofnięta.",
|
||||
"bulkDeleteTitle": "Usuń Wybrane Pliki",
|
||||
"actions": {
|
||||
"open": "Otwórz",
|
||||
"rename": "Zmień nazwę",
|
||||
"delete": "Usuń"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Brak plików lub folderów",
|
||||
"description": "Prześlij swój pierwszy plik lub utwórz folder, aby rozpocząć"
|
||||
},
|
||||
"files": "pliki",
|
||||
"folders": "foldery"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Tabela plików",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Usuń wybrane"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Edytuj folder",
|
||||
"folderName": "Nazwa folderu",
|
||||
"folderNamePlaceholder": "Wprowadź nazwę folderu",
|
||||
"folderDescription": "Opis",
|
||||
"folderDescriptionPlaceholder": "Wprowadź opis folderu (opcjonalnie)",
|
||||
"createFolder": "Utwórz nowy folder",
|
||||
"renameFolder": "Zmień nazwę folderu",
|
||||
"moveFolder": "Przenieś folder",
|
||||
"shareFolder": "Udostępnij folder",
|
||||
"deleteFolder": "Usuń folder",
|
||||
"moveTo": "Przenieś do",
|
||||
"selectDestination": "Wybierz folder docelowy",
|
||||
"rootFolder": "Katalog główny",
|
||||
"folderCreated": "Folder utworzony pomyślnie",
|
||||
"folderRenamed": "Nazwa folderu zmieniona pomyślnie",
|
||||
"folderMoved": "Folder przeniesiony pomyślnie",
|
||||
"folderDeleted": "Folder usunięty pomyślnie",
|
||||
"folderShared": "Folder udostępniony pomyślnie",
|
||||
"createFolderError": "Błąd podczas tworzenia folderu",
|
||||
"renameFolderError": "Błąd podczas zmiany nazwy folderu",
|
||||
"moveFolderError": "Błąd podczas przenoszenia folderu",
|
||||
"deleteFolderError": "Błąd podczas usuwania folderu",
|
||||
"shareFolderError": "Błąd podczas udostępniania folderu",
|
||||
"deleteConfirmation": "Czy na pewno chcesz usunąć ten folder?",
|
||||
"deleteWarning": "Ta operacja nie może zostać cofnięta."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Zasilane przez",
|
||||
"kyanHomepage": "Strona główna Kyantech"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Nie udało się usunąć logo"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Elementy do przeniesienia:",
|
||||
"movingTo": "Przenoszenie do:",
|
||||
"title": "Przenieś {count, plural, =1 {element} other {elementy}}",
|
||||
"description": "Przenieś {count, plural, =1 {element} other {elementy}} do nowej lokalizacji",
|
||||
"success": "Pomyślnie przeniesiono {count} {count, plural, =1 {element} other {elementów}}"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Logo aplikacji",
|
||||
"profileMenu": "Menu profilu",
|
||||
"profile": "Profil",
|
||||
"settings": "Ustawienia",
|
||||
"usersManagement": "Zarządzanie użytkownikami",
|
||||
"logout": "Wyloguj się"
|
||||
"logout": "Wyloguj się",
|
||||
"customization": "Dostosowanie"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Panel główny"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Powiadomienia o pobieraniu włączone",
|
||||
"permissionDenied": "Powiadomienia o pobieraniu wyłączone",
|
||||
"downloadComplete": {
|
||||
"title": "Pobieranie zakończone",
|
||||
"body": "Plik {fileName} został pobrany"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Błąd pobierania",
|
||||
"body": "Nie udało się pobrać pliku {fileName}: {error}",
|
||||
"unknownError": "Nieznany błąd"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Rozpoczęcie pobierania",
|
||||
"body": "Trwa pobieranie pliku {fileName}{position}",
|
||||
"position": " (był #{position} w kolejce)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Zmień hasło",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "Operacja kopiowania przekroczyła limit czasu. Spróbuj ponownie z mniejszym plikiem lub sprawdź swoje połączenie.",
|
||||
"failed": "Operacja kopiowania nie powiodła się. Spróbuj ponownie.",
|
||||
"aborted": "Operacja kopiowania została anulowana z powodu przekroczenia limitu czasu."
|
||||
}
|
||||
},
|
||||
"invalidDate": "Nieprawidłowa data"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Szukaj plików...",
|
||||
"results": "Znaleziono {filtered} z {total} plików"
|
||||
"results": "Znaleziono {filtered} z {total} plików",
|
||||
"placeholderFolders": "Szukaj folderów...",
|
||||
"noResults": "Nie znaleziono wyników dla \"{query}\"",
|
||||
"placeholderFiles": "Szukaj plików..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1223,7 +1382,18 @@
|
||||
"editSuccess": "Udostępnienie zaktualizowane pomyślnie",
|
||||
"editError": "Nie udało się zaktualizować udostępnienia",
|
||||
"bulkDeleteConfirmation": "Czy na pewno chcesz usunąć {count, plural, =1 {1 udostępnienie} other {# udostępnień}}? Tej operacji nie można cofnąć.",
|
||||
"bulkDeleteTitle": "Usuń wybrane udostępnienia"
|
||||
"bulkDeleteTitle": "Usuń wybrane udostępnienia",
|
||||
"addDescriptionPlaceholder": "Dodaj opis...",
|
||||
"aliasLabel": "Alias linku",
|
||||
"aliasPlaceholder": "Wprowadź niestandardowy alias",
|
||||
"copyLink": "Kopiuj link",
|
||||
"fileTitle": "Udostępnij plik",
|
||||
"folderTitle": "Udostępnij folder",
|
||||
"generateLink": "Generuj link",
|
||||
"linkDescriptionFile": "Wygeneruj niestandardowy link do udostępnienia pliku",
|
||||
"linkDescriptionFolder": "Wygeneruj niestandardowy link do udostępnienia folderu",
|
||||
"linkReady": "Twój link udostępniania jest gotowy:",
|
||||
"linkTitle": "Generuj link"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Szczegóły udostępnienia",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "plików",
|
||||
"totalSize": "Całkowity rozmiar",
|
||||
"creating": "Tworzenie...",
|
||||
"create": "Utwórz udostępnienie"
|
||||
"create": "Utwórz udostępnienie",
|
||||
"itemsToShare": "Elementy do udostępnienia ({count} {count, plural, =1 {element} other {elementów}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"title": "Ustawienia bezpieczeństwa udostępniania",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "Pobierz wybrany"
|
||||
},
|
||||
"selectAll": "Zaznacz wszystko",
|
||||
"selectShare": "Wybierz udostępnienie {shareName}"
|
||||
"selectShare": "Wybierz udostępnienie {shareName}",
|
||||
"folderCount": "foldery"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "Użycie pamięci",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "Atualizar",
|
||||
"creating": "Criando...",
|
||||
"click": "Clique para",
|
||||
"loadingSimple": "Carregando..."
|
||||
"loadingSimple": "Carregando...",
|
||||
"create": "Criar",
|
||||
"deleting": "Excluindo...",
|
||||
"move": "Mover",
|
||||
"rename": "Renomear",
|
||||
"search": "Pesquisar",
|
||||
"share": "Compartilhar"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Criar compartilhamento",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "Criar compartilhamento",
|
||||
"success": "Compartilhamento criado com sucesso",
|
||||
"error": "Falha ao criar compartilhamento",
|
||||
"namePlaceholder": "Digite um nome para seu compartilhamento"
|
||||
"namePlaceholder": "Digite um nome para seu compartilhamento",
|
||||
"nextSelectFiles": "Próximo: Selecionar arquivos",
|
||||
"searchLabel": "Pesquisar",
|
||||
"tabs": {
|
||||
"shareDetails": "Detalhes do compartilhamento",
|
||||
"selectFiles": "Selecionar arquivos"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Personalização",
|
||||
"colors": {
|
||||
"title": "Cores do Tema",
|
||||
"description": "Escolha sua cor primária preferida para o tema",
|
||||
"presets": "Cores Disponíveis",
|
||||
"presetsDescription": "Selecione entre os temas de cores disponíveis",
|
||||
"reset": "Restaurar Padrão"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Tipografia",
|
||||
"description": "Escolha sua família de fontes preferida",
|
||||
"available": "Fontes Disponíveis",
|
||||
"availableDescription": "Selecione entre as famílias de fontes disponíveis",
|
||||
"reset": "Restaurar Padrão"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Borda Arredondada",
|
||||
"description": "Personalize o arredondamento dos elementos da interface",
|
||||
"available": "Opções de Arredondamento",
|
||||
"availableDescription": "Escolha como os cantos arredondados devem aparecer",
|
||||
"reset": "Restaurar Padrão"
|
||||
},
|
||||
"background": {
|
||||
"title": "Cores de Fundo",
|
||||
"description": "Personalize as cores de fundo para os modos claro e escuro",
|
||||
"lightMode": "Modo Claro",
|
||||
"darkMode": "Modo Escuro",
|
||||
"availableDescription": "Escolha as cores de fundo para os temas claro e escuro",
|
||||
"reset": "Restaurar Padrão"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Modo do Tema",
|
||||
"description": "Escolha entre tema claro, escuro ou do sistema",
|
||||
"selectTheme": "Preferência de Tema",
|
||||
"availableDescription": "Selecione seu modo de tema preferido",
|
||||
"reset": "Restaurar para Sistema"
|
||||
},
|
||||
"pageTitle": "Personalização"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Falha ao carregar dados do painel",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Arquivos que serão excluídos",
|
||||
"sharesToDelete": "Compartilhamentos que serão excluídos"
|
||||
"sharesToDelete": "Compartilhamentos que serão excluídos",
|
||||
"foldersToDelete": "Pastas a serem excluídas",
|
||||
"itemsToDelete": "Itens a serem excluídos"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Download na fila: {fileName}",
|
||||
"queuedDescription": "Seu download começará automaticamente quando uma vaga estiver disponível",
|
||||
"queuePosition": "Download na posição {position} da fila: {fileName}",
|
||||
"estimatedWait": "Tempo estimado de espera: {time}",
|
||||
"queueFull": "Fila de download está cheia",
|
||||
"queueFullDescription": "Por favor, tente novamente em alguns minutos quando houver espaço na fila",
|
||||
"cancelSuccess": "Download cancelado com sucesso",
|
||||
"cancelError": "Falha ao cancelar download: {error}",
|
||||
"status": {
|
||||
"pending": "Preparando...",
|
||||
"queued": "Na fila",
|
||||
"downloading": "Baixando",
|
||||
"completed": "Concluído",
|
||||
"failed": "Falhou"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}s",
|
||||
"minutes": "{minutes}m",
|
||||
"hoursMinutes": "{hours}h {minutes}m"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Downloads",
|
||||
"downloads": "Fila de Download",
|
||||
"active": "Ativos",
|
||||
"queued": "Na Fila",
|
||||
"position": "Posição {position}",
|
||||
"estimatedWait": "Espera: {time}",
|
||||
"unknownFile": "Arquivo desconhecido",
|
||||
"noDownloads": "Nenhum download em andamento",
|
||||
"refresh": "Atualizar Fila"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Nenhum arquivo enviado ainda",
|
||||
@@ -201,6 +288,7 @@
|
||||
"extension": "Extensão",
|
||||
"descriptionLabel": "Descrição",
|
||||
"descriptionPlaceholder": "Digite a descrição do arquivo",
|
||||
"addDescriptionPlaceholder": "Adicionar descrição...",
|
||||
"deleteFile": "Excluir arquivo",
|
||||
"deleteConfirmation": "Tem certeza que deseja excluir ?",
|
||||
"deleteWarning": "Esta ação não pode ser desfeita."
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Visualizar arquivo",
|
||||
"addToShare": "Adicionar ao compartilhamento",
|
||||
"removeFromShare": "Remover do compartilhamento",
|
||||
"saveChanges": "Salvar Alterações"
|
||||
"saveChanges": "Salvar Alterações",
|
||||
"editFolder": "Editar pasta"
|
||||
},
|
||||
"files": {
|
||||
"title": "Todos os Arquivos",
|
||||
@@ -267,11 +356,24 @@
|
||||
"bulkDownloadFileError": "Erro ao baixar arquivo {fileName}",
|
||||
"bulkDeleteSuccess": "{count, plural, =1 {1 arquivo excluído com sucesso} other {# arquivos excluídos com sucesso}}",
|
||||
"bulkDeleteError": "Erro ao excluir arquivos selecionados",
|
||||
"bulkDeleteTitle": "Excluir Arquivos Selecionados",
|
||||
"bulkDeleteConfirmation": "Tem certeza que deseja excluir {count, plural, =1 {1 arquivo} other {# arquivos}}? Esta ação não pode ser desfeita.",
|
||||
"totalFiles": "{count, plural, =0 {Nenhum arquivo} =1 {1 arquivo} other {# arquivos}}",
|
||||
"viewMode": {
|
||||
"table": "Tabela",
|
||||
"grid": "Grade"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"open": "Abrir",
|
||||
"rename": "Renomear",
|
||||
"delete": "Excluir"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Nenhum arquivo ou pasta ainda",
|
||||
"description": "Carregue seu primeiro arquivo ou crie uma pasta para começar"
|
||||
},
|
||||
"files": "arquivos",
|
||||
"folders": "pastas"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Tabela de arquivos",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Excluir selecionados"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Editar pasta",
|
||||
"folderName": "Nome da pasta",
|
||||
"folderNamePlaceholder": "Digite o nome da pasta",
|
||||
"folderDescription": "Descrição",
|
||||
"folderDescriptionPlaceholder": "Digite a descrição da pasta (opcional)",
|
||||
"createFolder": "Criar nova pasta",
|
||||
"renameFolder": "Renomear pasta",
|
||||
"moveFolder": "Mover pasta",
|
||||
"shareFolder": "Compartilhar pasta",
|
||||
"deleteFolder": "Excluir pasta",
|
||||
"moveTo": "Mover para",
|
||||
"selectDestination": "Selecionar pasta de destino",
|
||||
"rootFolder": "Raiz",
|
||||
"folderCreated": "Pasta criada com sucesso",
|
||||
"folderRenamed": "Pasta renomeada com sucesso",
|
||||
"folderMoved": "Pasta movida com sucesso",
|
||||
"folderDeleted": "Pasta excluída com sucesso",
|
||||
"folderShared": "Pasta compartilhada com sucesso",
|
||||
"createFolderError": "Erro ao criar pasta",
|
||||
"renameFolderError": "Erro ao renomear pasta",
|
||||
"moveFolderError": "Erro ao mover pasta",
|
||||
"deleteFolderError": "Erro ao excluir pasta",
|
||||
"shareFolderError": "Erro ao compartilhar pasta",
|
||||
"deleteConfirmation": "Tem certeza de que deseja excluir esta pasta?",
|
||||
"deleteWarning": "Esta ação não pode ser desfeita."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Desenvolvido por",
|
||||
"kyanHomepage": "Página inicial da Kyantech"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Falha ao remover logo"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Itens para mover:",
|
||||
"movingTo": "Movendo para:",
|
||||
"title": "Mover {count, plural, =1 {item} other {itens}}",
|
||||
"description": "Mover {count, plural, =1 {item} other {itens}} para um novo local",
|
||||
"success": "Movidos com sucesso {count} {count, plural, =1 {item} other {itens}}"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Logo do aplicativo",
|
||||
"profileMenu": "Menu do Perfil",
|
||||
"profile": "Perfil",
|
||||
"settings": "Configurações",
|
||||
"usersManagement": "Gerenciar usuários",
|
||||
"logout": "Sair"
|
||||
"logout": "Sair",
|
||||
"customization": "Personalização"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Painel"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Notificações de download ativadas",
|
||||
"permissionDenied": "Notificações de download desativadas",
|
||||
"downloadComplete": {
|
||||
"title": "Download Concluído",
|
||||
"body": "{fileName} terminou de baixar"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Download Falhou",
|
||||
"body": "Falha ao baixar {fileName}: {error}",
|
||||
"unknownError": "Erro desconhecido"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Download Iniciando",
|
||||
"body": "{fileName} está sendo baixado agora{position}",
|
||||
"position": " (estava na posição #{position} da fila)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Alterar Senha",
|
||||
@@ -756,6 +911,7 @@
|
||||
"size": "Tamanho",
|
||||
"sender": "Enviado por",
|
||||
"date": "Data",
|
||||
"invalidDate": "Data inválida",
|
||||
"actions": "Ações"
|
||||
},
|
||||
"actions": {
|
||||
@@ -795,7 +951,8 @@
|
||||
"timeout": "A operação de cópia expirou. Por favor, tente novamente com um arquivo menor ou verifique sua conexão.",
|
||||
"failed": "A operação de cópia falhou. Por favor, tente novamente.",
|
||||
"aborted": "A operação de cópia foi cancelada devido ao tempo limite."
|
||||
}
|
||||
},
|
||||
"invalidDate": "Data inválida"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1167,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Buscar arquivos...",
|
||||
"results": "Encontrados {filtered} de {total} arquivos"
|
||||
"results": "Encontrados {filtered} de {total} arquivos",
|
||||
"placeholderFolders": "Pesquisar pastas...",
|
||||
"noResults": "Nenhum resultado encontrado para \"{query}\"",
|
||||
"placeholderFiles": "Buscar arquivos..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1205,6 +1365,7 @@
|
||||
"shareActions": {
|
||||
"deleteTitle": "Excluir Compartilhamento",
|
||||
"deleteConfirmation": "Tem certeza que deseja excluir este compartilhamento? Esta ação não pode ser desfeita.",
|
||||
"addDescriptionPlaceholder": "Adicionar descrição...",
|
||||
"bulkDeleteTitle": "Excluir Compartilhamentos Selecionados",
|
||||
"bulkDeleteConfirmation": "Tem certeza que deseja excluir {count, plural, =1 {1 compartilhamento} other {# compartilhamentos}} selecionado(s)? Esta ação não pode ser desfeita.",
|
||||
"editTitle": "Editar Compartilhamento",
|
||||
@@ -1223,7 +1384,17 @@
|
||||
"manageFilesTitle": "Gerenciar Arquivos",
|
||||
"manageRecipientsTitle": "Gerenciar Destinatários",
|
||||
"editSuccess": "Compartilhamento atualizado com sucesso",
|
||||
"editError": "Falha ao atualizar compartilhamento"
|
||||
"editError": "Falha ao atualizar compartilhamento",
|
||||
"aliasLabel": "Alias do link",
|
||||
"aliasPlaceholder": "Digite alias personalizado",
|
||||
"copyLink": "Copiar link",
|
||||
"fileTitle": "Compartilhar arquivo",
|
||||
"folderTitle": "Compartilhar pasta",
|
||||
"generateLink": "Gerar link",
|
||||
"linkDescriptionFile": "Gere um link personalizado para compartilhar o arquivo",
|
||||
"linkDescriptionFolder": "Gere um link personalizado para compartilhar a pasta",
|
||||
"linkReady": "Seu link de compartilhamento está pronto:",
|
||||
"linkTitle": "Gerar link"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Detalhes do Compartilhamento",
|
||||
@@ -1350,7 +1521,8 @@
|
||||
"files": "arquivos",
|
||||
"totalSize": "Tamanho total",
|
||||
"creating": "Criando...",
|
||||
"create": "Criar Compartilhamento"
|
||||
"create": "Criar Compartilhamento",
|
||||
"itemsToShare": "Itens para compartilhar ({count} {count, plural, =1 {item} other {itens}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "Configurar proteção por senha e opções de segurança para este compartilhamento",
|
||||
@@ -1455,7 +1627,8 @@
|
||||
"delete": "Excluir",
|
||||
"downloadShareFiles": "Baixar todos os arquivos",
|
||||
"viewQrCode": "Visualizar QR Code"
|
||||
}
|
||||
},
|
||||
"folderCount": "pastas"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "Uso de armazenamento",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "Обновить",
|
||||
"click": "Нажмите для",
|
||||
"creating": "Создание...",
|
||||
"loadingSimple": "Загрузка..."
|
||||
"loadingSimple": "Загрузка...",
|
||||
"create": "Создать",
|
||||
"deleting": "Удаление...",
|
||||
"move": "Переместить",
|
||||
"rename": "Переименовать",
|
||||
"search": "Поиск",
|
||||
"share": "Поделиться"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Создать общий доступ",
|
||||
@@ -160,7 +166,53 @@
|
||||
"error": "Не удалось создать общий доступ",
|
||||
"descriptionLabel": "Описание",
|
||||
"descriptionPlaceholder": "Введите описание (опционально)",
|
||||
"namePlaceholder": "Введите имя для вашего общего доступа"
|
||||
"namePlaceholder": "Введите имя для вашего общего доступа",
|
||||
"nextSelectFiles": "Далее: Выбор файлов",
|
||||
"searchLabel": "Поиск",
|
||||
"tabs": {
|
||||
"shareDetails": "Детали общего доступа",
|
||||
"selectFiles": "Выбрать файлы"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Настройка",
|
||||
"colors": {
|
||||
"title": "Цвета темы",
|
||||
"description": "Выберите предпочтительный основной цвет темы",
|
||||
"presets": "Доступные цвета",
|
||||
"presetsDescription": "Выберите из доступных цветовых тем",
|
||||
"reset": "Сбросить по умолчанию"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Типография",
|
||||
"description": "Выберите предпочтительное семейство шрифтов",
|
||||
"available": "Доступные шрифты",
|
||||
"availableDescription": "Выберите из доступных семейств шрифтов",
|
||||
"reset": "Сбросить по умолчанию"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Радиус границ",
|
||||
"description": "Настройте закругление элементов интерфейса",
|
||||
"available": "Варианты закругления",
|
||||
"availableDescription": "Выберите, как должны выглядеть закругленные углы",
|
||||
"reset": "Сбросить по умолчанию"
|
||||
},
|
||||
"background": {
|
||||
"title": "Цвета фона",
|
||||
"description": "Настройте цвета фона для светлого и темного режимов",
|
||||
"lightMode": "Светлый режим",
|
||||
"darkMode": "Темный режим",
|
||||
"availableDescription": "Выберите цвета фона для светлой и темной тем",
|
||||
"reset": "Сбросить по умолчанию"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Режим темы",
|
||||
"description": "Выберите между светлой, темной или системной темой",
|
||||
"selectTheme": "Предпочтительная тема",
|
||||
"availableDescription": "Выберите предпочтительный режим темы",
|
||||
"reset": "Сбросить на системную"
|
||||
},
|
||||
"pageTitle": "Настройка"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Ошибка загрузки данных панели управления",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Файлы для удаления",
|
||||
"sharesToDelete": "Общие папки, которые будут удалены"
|
||||
"sharesToDelete": "Общие папки, которые будут удалены",
|
||||
"foldersToDelete": "Папки для удаления",
|
||||
"itemsToDelete": "Элементы для удаления"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "Загрузка в очереди: {fileName}",
|
||||
"queuedDescription": "Ваша загрузка начнется автоматически, когда освободится слот",
|
||||
"queuePosition": "Загрузка в очереди на позиции {position}: {fileName}",
|
||||
"estimatedWait": "Расчетное время ожидания: {time}",
|
||||
"queueFull": "Очередь загрузки заполнена",
|
||||
"queueFullDescription": "Пожалуйста, повторите попытку через несколько минут, когда в очереди появится место",
|
||||
"cancelSuccess": "Загрузка успешно отменена",
|
||||
"cancelError": "Не удалось отменить загрузку: {error}",
|
||||
"status": {
|
||||
"pending": "Подготовка...",
|
||||
"queued": "В очереди",
|
||||
"downloading": "Загрузка",
|
||||
"completed": "Завершено",
|
||||
"failed": "Не удалось"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds} сек",
|
||||
"minutes": "{minutes} мин",
|
||||
"hoursMinutes": "{hours}ч {minutes}мин"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "Загрузки",
|
||||
"downloads": "Очередь загрузки",
|
||||
"active": "Активные",
|
||||
"queued": "В очереди",
|
||||
"position": "Позиция {position}",
|
||||
"estimatedWait": "Ожидание: {time}",
|
||||
"unknownFile": "Неизвестный файл",
|
||||
"noDownloads": "Нет активных загрузок",
|
||||
"refresh": "Обновить очередь"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Файлы еще не загружены",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "Введите описание файла",
|
||||
"deleteFile": "Удалить файл",
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить ?",
|
||||
"deleteWarning": "Это действие необратимо."
|
||||
"deleteWarning": "Это действие необратимо.",
|
||||
"addDescriptionPlaceholder": "Добавить описание..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "Ошибка скачивания файла",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Предпросмотр файла",
|
||||
"addToShare": "Добавить в общий доступ",
|
||||
"removeFromShare": "Удалить из общего доступа",
|
||||
"saveChanges": "Сохранить Изменения"
|
||||
"saveChanges": "Сохранить Изменения",
|
||||
"editFolder": "Редактировать папку"
|
||||
},
|
||||
"files": {
|
||||
"title": "Все файлы",
|
||||
@@ -271,7 +360,20 @@
|
||||
"viewMode": {
|
||||
"table": "Таблица",
|
||||
"grid": "Сетка"
|
||||
}
|
||||
},
|
||||
"bulkDeleteConfirmation": "Вы уверены, что хотите удалить {count, plural, =1 {1 файл} other {# файлов}}? Это действие нельзя отменить.",
|
||||
"bulkDeleteTitle": "Удалить Выбранные Файлы",
|
||||
"actions": {
|
||||
"open": "Открыть",
|
||||
"rename": "Переименовать",
|
||||
"delete": "Удалить"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Пока нет файлов или папок",
|
||||
"description": "Загрузите свой первый файл или создайте папку для начала работы"
|
||||
},
|
||||
"files": "файлы",
|
||||
"folders": "папки"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Таблица файлов",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Удалить Выбранные"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Редактировать папку",
|
||||
"folderName": "Имя папки",
|
||||
"folderNamePlaceholder": "Введите имя папки",
|
||||
"folderDescription": "Описание",
|
||||
"folderDescriptionPlaceholder": "Введите описание папки (необязательно)",
|
||||
"createFolder": "Создать новую папку",
|
||||
"renameFolder": "Переименовать папку",
|
||||
"moveFolder": "Переместить папку",
|
||||
"shareFolder": "Поделиться папкой",
|
||||
"deleteFolder": "Удалить папку",
|
||||
"moveTo": "Переместить в",
|
||||
"selectDestination": "Выберите папку назначения",
|
||||
"rootFolder": "Корень",
|
||||
"folderCreated": "Папка успешно создана",
|
||||
"folderRenamed": "Папка успешно переименована",
|
||||
"folderMoved": "Папка успешно перемещена",
|
||||
"folderDeleted": "Папка успешно удалена",
|
||||
"folderShared": "Папка успешно отправлена",
|
||||
"createFolderError": "Ошибка создания папки",
|
||||
"renameFolderError": "Ошибка переименования папки",
|
||||
"moveFolderError": "Ошибка перемещения папки",
|
||||
"deleteFolderError": "Ошибка удаления папки",
|
||||
"shareFolderError": "Ошибка обмена папкой",
|
||||
"deleteConfirmation": "Вы уверены, что хотите удалить эту папку?",
|
||||
"deleteWarning": "Это действие нельзя отменить."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "При поддержке",
|
||||
"kyanHomepage": "Домашняя страница Kyantech"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Ошибка удаления логотипа"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Элементы для перемещения:",
|
||||
"movingTo": "Перемещение в:",
|
||||
"title": "Переместить {count, plural, =1 {элемент} other {элементы}}",
|
||||
"description": "Переместить {count, plural, =1 {элемент} other {элементы}} в новое место",
|
||||
"success": "Успешно перемещено {count} {count, plural, =1 {элемент} other {элементов}}"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Логотип приложения",
|
||||
"profileMenu": "Меню профиля",
|
||||
"profile": "Профиль",
|
||||
"settings": "Настройки",
|
||||
"usersManagement": "Управление пользователями",
|
||||
"logout": "Выйти"
|
||||
"logout": "Выйти",
|
||||
"customization": "Настройки"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Панель управления"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "Уведомления о загрузках включены",
|
||||
"permissionDenied": "Уведомления о загрузках отключены",
|
||||
"downloadComplete": {
|
||||
"title": "Загрузка завершена",
|
||||
"body": "Файл {fileName} успешно загружен"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "Ошибка загрузки",
|
||||
"body": "Не удалось загрузить {fileName}: {error}",
|
||||
"unknownError": "Неизвестная ошибка"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "Начало загрузки",
|
||||
"body": "Файл {fileName} загружается{position}",
|
||||
"position": " (был №{position} в очереди)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Изменить пароль",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "Время операции копирования истекло. Пожалуйста, попробуйте еще раз с файлом меньшего размера или проверьте подключение.",
|
||||
"failed": "Ошибка операции копирования. Пожалуйста, попробуйте еще раз.",
|
||||
"aborted": "Операция копирования была отменена из-за истечения времени ожидания."
|
||||
}
|
||||
},
|
||||
"invalidDate": "Неверная дата"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Поиск файлов...",
|
||||
"results": "Найдено {filtered} из {total} файлов"
|
||||
"results": "Найдено {filtered} из {total} файлов",
|
||||
"placeholderFolders": "Поиск папок...",
|
||||
"noResults": "Не найдено результатов для \"{query}\"",
|
||||
"placeholderFiles": "Поиск файлов..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1223,7 +1382,18 @@
|
||||
"editSuccess": "Общий доступ успешно обновлен",
|
||||
"editError": "Ошибка обновления общего доступа",
|
||||
"bulkDeleteConfirmation": "Вы уверены, что хотите удалить {count, plural, =1 {1 общую папку} other {# общих папок}}? Это действие нельзя отменить.",
|
||||
"bulkDeleteTitle": "Удалить Выбранные Общие Папки"
|
||||
"bulkDeleteTitle": "Удалить Выбранные Общие Папки",
|
||||
"addDescriptionPlaceholder": "Добавить описание...",
|
||||
"aliasLabel": "Псевдоним ссылки",
|
||||
"aliasPlaceholder": "Введите пользовательский псевдоним",
|
||||
"copyLink": "Копировать ссылку",
|
||||
"fileTitle": "Поделиться файлом",
|
||||
"folderTitle": "Поделиться папкой",
|
||||
"generateLink": "Создать ссылку",
|
||||
"linkDescriptionFile": "Создайте пользовательскую ссылку для обмена файлом",
|
||||
"linkDescriptionFolder": "Создайте пользовательскую ссылку для обмена папкой",
|
||||
"linkReady": "Ваша ссылка для обмена готова:",
|
||||
"linkTitle": "Создать ссылку"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Детали Общего Доступа",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "файлов",
|
||||
"totalSize": "Общий размер",
|
||||
"creating": "Создание...",
|
||||
"create": "Создать Общий Доступ"
|
||||
"create": "Создать Общий Доступ",
|
||||
"itemsToShare": "Элементы для обмена ({count} {count, plural, =1 {элемент} other {элементов}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "Настройте защиту паролем и параметры безопасности для этого общего доступа",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "Скачать выбранный"
|
||||
},
|
||||
"selectAll": "Выбрать все",
|
||||
"selectShare": "Выбрать общую папку {shareName}"
|
||||
"selectShare": "Выбрать общую папку {shareName}",
|
||||
"folderCount": "папки"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "Использование хранилища",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "Güncelle",
|
||||
"click": "Tıklayın",
|
||||
"creating": "Oluşturuluyor...",
|
||||
"loadingSimple": "Yükleniyor..."
|
||||
"loadingSimple": "Yükleniyor...",
|
||||
"create": "Oluştur",
|
||||
"deleting": "Siliniyor...",
|
||||
"move": "Taşı",
|
||||
"rename": "Yeniden Adlandır",
|
||||
"search": "Ara",
|
||||
"share": "Paylaş"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "Paylaşım Oluştur",
|
||||
@@ -160,7 +166,53 @@
|
||||
"error": "Paylaşım oluşturulamadı",
|
||||
"descriptionLabel": "Açıklama",
|
||||
"descriptionPlaceholder": "Açıklama girin (isteğe bağlı)",
|
||||
"namePlaceholder": "Paylaşımınız için bir ad girin"
|
||||
"namePlaceholder": "Paylaşımınız için bir ad girin",
|
||||
"nextSelectFiles": "İleri: Dosyaları Seç",
|
||||
"searchLabel": "Ara",
|
||||
"tabs": {
|
||||
"shareDetails": "Paylaşım Detayları",
|
||||
"selectFiles": "Dosyaları Seç"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "Özelleştirme",
|
||||
"colors": {
|
||||
"title": "Tema Renkleri",
|
||||
"description": "Tercih ettiğiniz birincil renk temasını seçin",
|
||||
"presets": "Mevcut Renkler",
|
||||
"presetsDescription": "Mevcut renk temalarından seçim yapın",
|
||||
"reset": "Varsayılana Sıfırla"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "Tipografi",
|
||||
"description": "Tercih ettiğiniz yazı tipini seçin",
|
||||
"available": "Mevcut Yazı Tipleri",
|
||||
"availableDescription": "Mevcut yazı tipi ailelerinden seçim yapın",
|
||||
"reset": "Varsayılana Sıfırla"
|
||||
},
|
||||
"radius": {
|
||||
"title": "Kenar Yuvarlaklığı",
|
||||
"description": "Arayüz öğelerinin yuvarlaklığını özelleştirin",
|
||||
"available": "Yuvarlaklık Seçenekleri",
|
||||
"availableDescription": "Köşelerin ne kadar yuvarlak görüneceğini seçin",
|
||||
"reset": "Varsayılana Sıfırla"
|
||||
},
|
||||
"background": {
|
||||
"title": "Arka Plan Renkleri",
|
||||
"description": "Açık ve karanlık modlar için arka plan renklerini özelleştirin",
|
||||
"lightMode": "Açık Mod",
|
||||
"darkMode": "Karanlık Mod",
|
||||
"availableDescription": "Hem açık hem de karanlık temalar için arka plan renklerini seçin",
|
||||
"reset": "Varsayılana Sıfırla"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Tema Modu",
|
||||
"description": "Açık, karanlık veya sistem teması arasından seçim yapın",
|
||||
"selectTheme": "Tema Tercihi",
|
||||
"availableDescription": "Tercih ettiğiniz tema modunu seçin",
|
||||
"reset": "Sisteme Sıfırla"
|
||||
},
|
||||
"pageTitle": "Özelleştirme"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "Gösterge paneli verileri yüklenemedi",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "Silinecek dosyalar",
|
||||
"sharesToDelete": "Silinecek paylaşımlar"
|
||||
"sharesToDelete": "Silinecek paylaşımlar",
|
||||
"foldersToDelete": "Silinecek klasörler",
|
||||
"itemsToDelete": "Silinecek öğeler"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "İndirme sıraya alındı: {fileName}",
|
||||
"queuedDescription": "Bir slot uygun hale geldiğinde indirmeniz otomatik olarak başlayacak",
|
||||
"queuePosition": "{position}. sırada indirme: {fileName}",
|
||||
"estimatedWait": "Tahmini bekleme süresi: {time}",
|
||||
"queueFull": "İndirme kuyruğu dolu",
|
||||
"queueFullDescription": "Lütfen kuyrukta yer açıldığında birkaç dakika sonra tekrar deneyin",
|
||||
"cancelSuccess": "İndirme başarıyla iptal edildi",
|
||||
"cancelError": "İndirme iptal edilemedi: {error}",
|
||||
"status": {
|
||||
"pending": "Hazırlanıyor...",
|
||||
"queued": "Kuyrukta",
|
||||
"downloading": "İndiriliyor",
|
||||
"completed": "Tamamlandı",
|
||||
"failed": "Başarısız"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}sn",
|
||||
"minutes": "{minutes}dk",
|
||||
"hoursMinutes": "{hours}sa {minutes}dk"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "İndirmeler",
|
||||
"downloads": "İndirme Kuyruğu",
|
||||
"active": "Aktif",
|
||||
"queued": "Sırada",
|
||||
"position": "Konum {position}",
|
||||
"estimatedWait": "Bekleme: {time}",
|
||||
"unknownFile": "Bilinmeyen dosya",
|
||||
"noDownloads": "Devam eden indirme yok",
|
||||
"refresh": "Kuyruğu Yenile"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "Henüz dosya yüklenmedi",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "Dosya açıklamasını girin",
|
||||
"deleteFile": "Dosyayı Sil",
|
||||
"deleteConfirmation": " dosyasını silmek istediğinize emin misiniz?",
|
||||
"deleteWarning": "Bu işlem geri alınamaz."
|
||||
"deleteWarning": "Bu işlem geri alınamaz.",
|
||||
"addDescriptionPlaceholder": "Açıklama ekle..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "Dosya indirilemedi",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "Dosyayı önizle",
|
||||
"addToShare": "Paylaşıma ekle",
|
||||
"removeFromShare": "Paylaşımdan kaldır",
|
||||
"saveChanges": "Değişiklikleri Kaydet"
|
||||
"saveChanges": "Değişiklikleri Kaydet",
|
||||
"editFolder": "Klasörü düzenle"
|
||||
},
|
||||
"files": {
|
||||
"title": "Tüm Dosyalar",
|
||||
@@ -271,7 +360,20 @@
|
||||
"viewMode": {
|
||||
"table": "Tablo",
|
||||
"grid": "Izgara"
|
||||
}
|
||||
},
|
||||
"bulkDeleteConfirmation": "{count, plural, =1 {1 dosyayı} other {# dosyayı}} silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||
"bulkDeleteTitle": "Seçili Dosyaları Sil",
|
||||
"actions": {
|
||||
"open": "Aç",
|
||||
"rename": "Yeniden Adlandır",
|
||||
"delete": "Sil"
|
||||
},
|
||||
"empty": {
|
||||
"title": "Henüz dosya veya klasör yok",
|
||||
"description": "Başlamak için ilk dosyanızı yükleyin veya bir klasör oluşturun"
|
||||
},
|
||||
"files": "dosyalar",
|
||||
"folders": "klasörler"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "Dosya Tablosu",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "Seçilenleri Sil"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "Klasörü düzenle",
|
||||
"folderName": "Klasör Adı",
|
||||
"folderNamePlaceholder": "Klasör adını girin",
|
||||
"folderDescription": "Açıklama",
|
||||
"folderDescriptionPlaceholder": "Klasör açıklaması girin (isteğe bağlı)",
|
||||
"createFolder": "Yeni Klasör Oluştur",
|
||||
"renameFolder": "Klasörü Yeniden Adlandır",
|
||||
"moveFolder": "Klasörü Taşı",
|
||||
"shareFolder": "Klasörü Paylaş",
|
||||
"deleteFolder": "Klasörü Sil",
|
||||
"moveTo": "Taşı",
|
||||
"selectDestination": "Hedef klasörü seç",
|
||||
"rootFolder": "Kök",
|
||||
"folderCreated": "Klasör başarıyla oluşturuldu",
|
||||
"folderRenamed": "Klasör başarıyla yeniden adlandırıldı",
|
||||
"folderMoved": "Klasör başarıyla taşındı",
|
||||
"folderDeleted": "Klasör başarıyla silindi",
|
||||
"folderShared": "Klasör başarıyla paylaşıldı",
|
||||
"createFolderError": "Klasör oluşturulurken hata",
|
||||
"renameFolderError": "Klasör yeniden adlandırılırken hata",
|
||||
"moveFolderError": "Klasör taşınırken hata",
|
||||
"deleteFolderError": "Klasör silinirken hata",
|
||||
"shareFolderError": "Klasör paylaşılırken hata",
|
||||
"deleteConfirmation": "Bu klasörü silmek istediğinizden emin misiniz?",
|
||||
"deleteWarning": "Bu işlem geri alınamaz."
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "Tarafından destekleniyor:",
|
||||
"kyanHomepage": "Kyantech Ana Sayfası"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Logo kaldırılamadı"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "Taşınacak öğeler:",
|
||||
"movingTo": "Taşınıyor:",
|
||||
"title": "{count, plural, =1 {Öğe} other {Öğeler}} Taşı",
|
||||
"description": "{count, plural, =1 {Öğeyi} other {Öğeleri}} yeni konuma taşı",
|
||||
"success": "{count} {count, plural, =1 {öğe} other {öğe}} başarıyla taşındı"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "Uygulama Logosu",
|
||||
"profileMenu": "Profil Menüsü",
|
||||
"profile": "Profil",
|
||||
"settings": "Ayarlar",
|
||||
"usersManagement": "Kullanıcı Yönetimi",
|
||||
"logout": "Oturumu Kapat"
|
||||
"logout": "Oturumu Kapat",
|
||||
"customization": "Özelleştirme"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "Gösterge Paneli"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "İndirme bildirimleri etkinleştirildi",
|
||||
"permissionDenied": "İndirme bildirimleri devre dışı bırakıldı",
|
||||
"downloadComplete": {
|
||||
"title": "İndirme Tamamlandı",
|
||||
"body": "{fileName} indirmesi tamamlandı"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "İndirme Başarısız",
|
||||
"body": "{fileName} indirilemedi: {error}",
|
||||
"unknownError": "Bilinmeyen hata"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "İndirme Başlıyor",
|
||||
"body": "{fileName} şimdi indiriliyor{position}",
|
||||
"position": " (kuyrukta #{position} sıradaydı)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "Şifreyi Değiştir",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "Kopyalama işlemi zaman aşımına uğradı. Lütfen daha küçük bir dosya ile tekrar deneyin veya bağlantınızı kontrol edin.",
|
||||
"failed": "Kopyalama işlemi başarısız oldu. Lütfen tekrar deneyin.",
|
||||
"aborted": "Kopyalama işlemi zaman aşımı nedeniyle iptal edildi."
|
||||
}
|
||||
},
|
||||
"invalidDate": "Geçersiz tarih"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "Dosya ara...",
|
||||
"results": "Toplam {total} dosya içinde {filtered} dosya bulundu"
|
||||
"results": "Toplam {total} dosya içinde {filtered} dosya bulundu",
|
||||
"placeholderFolders": "Klasörleri ara...",
|
||||
"noResults": "\"{query}\" için sonuç bulunamadı",
|
||||
"placeholderFiles": "Dosya ara..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1223,7 +1382,18 @@
|
||||
"editSuccess": "Paylaşım başarıyla güncellendi",
|
||||
"editError": "Paylaşım güncelleme başarısız",
|
||||
"bulkDeleteConfirmation": "{count, plural, =1 {1 paylaşımı} other {# paylaşımı}} silmek istediğinizden emin misiniz? Bu işlem geri alınamaz.",
|
||||
"bulkDeleteTitle": "Seçili Paylaşımları Sil"
|
||||
"bulkDeleteTitle": "Seçili Paylaşımları Sil",
|
||||
"addDescriptionPlaceholder": "Açıklama ekle...",
|
||||
"aliasLabel": "Bağlantı Takma Adı",
|
||||
"aliasPlaceholder": "Özel takma ad girin",
|
||||
"copyLink": "Bağlantıyı Kopyala",
|
||||
"fileTitle": "Dosyayı Paylaş",
|
||||
"folderTitle": "Klasörü Paylaş",
|
||||
"generateLink": "Bağlantı Oluştur",
|
||||
"linkDescriptionFile": "Dosyayı paylaşmak için özel bağlantı oluşturun",
|
||||
"linkDescriptionFolder": "Klasörü paylaşmak için özel bağlantı oluşturun",
|
||||
"linkReady": "Paylaşım bağlantınız hazır:",
|
||||
"linkTitle": "Bağlantı Oluştur"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "Paylaşım Detayları",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "dosya",
|
||||
"totalSize": "Toplam boyut",
|
||||
"creating": "Oluşturuluyor...",
|
||||
"create": "Paylaşım Oluştur"
|
||||
"create": "Paylaşım Oluştur",
|
||||
"itemsToShare": "Paylaşılacak öğeler ({count} {count, plural, =1 {öğe} other {öğe}})"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "Bu paylaşım için şifre koruması ve güvenlik seçeneklerini yapılandırın",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "Seçili indir"
|
||||
},
|
||||
"selectAll": "Tümünü seç",
|
||||
"selectShare": "Paylaşım {shareName} seç"
|
||||
"selectShare": "Paylaşım {shareName} seç",
|
||||
"folderCount": "klasörler"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "Depolama Kullanımı",
|
||||
|
@@ -144,7 +144,13 @@
|
||||
"update": "更新",
|
||||
"click": "点击",
|
||||
"creating": "创建中...",
|
||||
"loadingSimple": "加载中..."
|
||||
"loadingSimple": "加载中...",
|
||||
"create": "创建",
|
||||
"deleting": "删除中...",
|
||||
"move": "移动",
|
||||
"rename": "重命名",
|
||||
"search": "搜索",
|
||||
"share": "分享"
|
||||
},
|
||||
"createShare": {
|
||||
"title": "创建分享",
|
||||
@@ -160,7 +166,53 @@
|
||||
"create": "创建分享",
|
||||
"success": "分享创建成功",
|
||||
"error": "创建分享失败",
|
||||
"namePlaceholder": "输入分享名称"
|
||||
"namePlaceholder": "输入分享名称",
|
||||
"nextSelectFiles": "下一步:选择文件",
|
||||
"searchLabel": "搜索",
|
||||
"tabs": {
|
||||
"shareDetails": "分享详情",
|
||||
"selectFiles": "选择文件"
|
||||
}
|
||||
},
|
||||
"customization": {
|
||||
"breadcrumb": "自定义",
|
||||
"colors": {
|
||||
"title": "主题颜色",
|
||||
"description": "选择您喜欢的主题颜色",
|
||||
"presets": "可用颜色",
|
||||
"presetsDescription": "从可用的颜色主题中选择",
|
||||
"reset": "恢复默认值"
|
||||
},
|
||||
"fonts": {
|
||||
"title": "字体",
|
||||
"description": "选择您喜欢的字体系列",
|
||||
"available": "可用字体",
|
||||
"availableDescription": "从可用的字体系列中选择",
|
||||
"reset": "恢复默认值"
|
||||
},
|
||||
"radius": {
|
||||
"title": "边框圆角",
|
||||
"description": "自定义界面元素的圆角程度",
|
||||
"available": "圆角选项",
|
||||
"availableDescription": "选择圆角的显示方式",
|
||||
"reset": "恢复默认值"
|
||||
},
|
||||
"background": {
|
||||
"title": "背景颜色",
|
||||
"description": "自定义明暗模式的背景颜色",
|
||||
"lightMode": "明亮模式",
|
||||
"darkMode": "暗黑模式",
|
||||
"availableDescription": "为明暗主题选择背景颜色",
|
||||
"reset": "恢复默认值"
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题模式",
|
||||
"description": "在明亮、暗黑或系统主题之间选择",
|
||||
"selectTheme": "主题偏好",
|
||||
"availableDescription": "选择您喜欢的主题模式",
|
||||
"reset": "恢复系统设置"
|
||||
},
|
||||
"pageTitle": "自定义"
|
||||
},
|
||||
"dashboard": {
|
||||
"loadError": "加载仪表盘数据失败",
|
||||
@@ -174,7 +226,42 @@
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"filesToDelete": "要删除的文件",
|
||||
"sharesToDelete": "将被删除的共享"
|
||||
"sharesToDelete": "将被删除的共享",
|
||||
"foldersToDelete": "要删除的文件夹",
|
||||
"itemsToDelete": "要删除的项目"
|
||||
},
|
||||
"downloadQueue": {
|
||||
"downloadQueued": "已加入下载队列:{fileName}",
|
||||
"queuedDescription": "当有空闲位置时,您的下载将自动开始",
|
||||
"queuePosition": "在队列位置 {position} 等待下载:{fileName}",
|
||||
"estimatedWait": "预计等待时间:{time}",
|
||||
"queueFull": "下载队列已满",
|
||||
"queueFullDescription": "请等待几分钟后当队列有空位时再试",
|
||||
"cancelSuccess": "下载已成功取消",
|
||||
"cancelError": "取消下载失败:{error}",
|
||||
"status": {
|
||||
"pending": "准备中...",
|
||||
"queued": "排队中",
|
||||
"downloading": "下载中",
|
||||
"completed": "已完成",
|
||||
"failed": "失败"
|
||||
},
|
||||
"waitTime": {
|
||||
"seconds": "{seconds}秒",
|
||||
"minutes": "{minutes}分",
|
||||
"hoursMinutes": "{hours}小时 {minutes}分"
|
||||
},
|
||||
"indicator": {
|
||||
"title": "下载",
|
||||
"downloads": "下载队列",
|
||||
"active": "进行中",
|
||||
"queued": "排队中",
|
||||
"position": "位置 {position}",
|
||||
"estimatedWait": "等待:{time}",
|
||||
"unknownFile": "未知文件",
|
||||
"noDownloads": "当前没有下载任务",
|
||||
"refresh": "刷新队列"
|
||||
}
|
||||
},
|
||||
"emptyState": {
|
||||
"noFiles": "尚未上传任何文件",
|
||||
@@ -203,7 +290,8 @@
|
||||
"descriptionPlaceholder": "请输入文件描述",
|
||||
"deleteFile": "删除文件",
|
||||
"deleteConfirmation": "您确定要删除{fileName}吗?",
|
||||
"deleteWarning": "此操作不可撤销。"
|
||||
"deleteWarning": "此操作不可撤销。",
|
||||
"addDescriptionPlaceholder": "添加描述..."
|
||||
},
|
||||
"fileManager": {
|
||||
"downloadError": "文件下载失败",
|
||||
@@ -248,7 +336,8 @@
|
||||
"previewFile": "预览文件",
|
||||
"addToShare": "添加到共享",
|
||||
"removeFromShare": "从共享中移除",
|
||||
"saveChanges": "保存更改"
|
||||
"saveChanges": "保存更改",
|
||||
"editFolder": "编辑文件夹"
|
||||
},
|
||||
"files": {
|
||||
"title": "所有文件",
|
||||
@@ -271,7 +360,20 @@
|
||||
"table": "表格",
|
||||
"grid": "网格"
|
||||
},
|
||||
"totalFiles": "{count, plural, =0 {无文件} =1 {1个文件} other {#个文件}}"
|
||||
"totalFiles": "{count, plural, =0 {无文件} =1 {1个文件} other {#个文件}}",
|
||||
"bulkDeleteConfirmation": "您确定要删除 {count, plural, =1 {1 个文件} other {# 个文件}}吗?此操作无法撤销。",
|
||||
"bulkDeleteTitle": "删除所选文件",
|
||||
"actions": {
|
||||
"open": "打开",
|
||||
"rename": "重命名",
|
||||
"delete": "删除"
|
||||
},
|
||||
"empty": {
|
||||
"title": "还没有文件或文件夹",
|
||||
"description": "上传您的第一个文件或创建文件夹以开始使用"
|
||||
},
|
||||
"files": "文件",
|
||||
"folders": "文件夹"
|
||||
},
|
||||
"filesTable": {
|
||||
"ariaLabel": "文件表格",
|
||||
@@ -301,6 +403,33 @@
|
||||
"delete": "删除选中项"
|
||||
}
|
||||
},
|
||||
"folderActions": {
|
||||
"editFolder": "编辑文件夹",
|
||||
"folderName": "文件夹名称",
|
||||
"folderNamePlaceholder": "输入文件夹名称",
|
||||
"folderDescription": "描述",
|
||||
"folderDescriptionPlaceholder": "输入文件夹描述(可选)",
|
||||
"createFolder": "创建新文件夹",
|
||||
"renameFolder": "重命名文件夹",
|
||||
"moveFolder": "移动文件夹",
|
||||
"shareFolder": "分享文件夹",
|
||||
"deleteFolder": "删除文件夹",
|
||||
"moveTo": "移动到",
|
||||
"selectDestination": "选择目标文件夹",
|
||||
"rootFolder": "根目录",
|
||||
"folderCreated": "文件夹创建成功",
|
||||
"folderRenamed": "文件夹重命名成功",
|
||||
"folderMoved": "文件夹移动成功",
|
||||
"folderDeleted": "文件夹删除成功",
|
||||
"folderShared": "文件夹分享成功",
|
||||
"createFolderError": "创建文件夹时出错",
|
||||
"renameFolderError": "重命名文件夹时出错",
|
||||
"moveFolderError": "移动文件夹时出错",
|
||||
"deleteFolderError": "删除文件夹时出错",
|
||||
"shareFolderError": "分享文件夹时出错",
|
||||
"deleteConfirmation": "您确定要删除此文件夹吗?",
|
||||
"deleteWarning": "此操作无法撤销。"
|
||||
},
|
||||
"footer": {
|
||||
"poweredBy": "技术支持:",
|
||||
"kyanHomepage": "Kyantech 主页"
|
||||
@@ -402,17 +531,43 @@
|
||||
"removeFailed": "Logo删除失败"
|
||||
}
|
||||
},
|
||||
"moveItems": {
|
||||
"itemsToMove": "要移动的项目:",
|
||||
"movingTo": "移动到:",
|
||||
"title": "移动 {count, plural, =1 {项目} other {项目}}",
|
||||
"description": "将 {count, plural, =1 {项目} other {项目}} 移动到新位置",
|
||||
"success": "成功移动了 {count} 个项目"
|
||||
},
|
||||
"navbar": {
|
||||
"logoAlt": "应用Logo",
|
||||
"profileMenu": "个人菜单",
|
||||
"profile": "个人资料",
|
||||
"settings": "设置",
|
||||
"usersManagement": "用户管理",
|
||||
"logout": "退出登录"
|
||||
"logout": "退出登录",
|
||||
"customization": "自定义"
|
||||
},
|
||||
"navigation": {
|
||||
"dashboard": "仪表盘"
|
||||
},
|
||||
"notifications": {
|
||||
"permissionGranted": "下载通知已启用",
|
||||
"permissionDenied": "下载通知已禁用",
|
||||
"downloadComplete": {
|
||||
"title": "下载完成",
|
||||
"body": "{fileName} 已下载完成"
|
||||
},
|
||||
"downloadFailed": {
|
||||
"title": "下载失败",
|
||||
"body": "下载 {fileName} 失败:{error}",
|
||||
"unknownError": "未知错误"
|
||||
},
|
||||
"queueProcessing": {
|
||||
"title": "开始下载",
|
||||
"body": "{fileName} 正在下载{position}",
|
||||
"position": "(队列中第 {position} 位)"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"password": {
|
||||
"title": "修改密码",
|
||||
@@ -795,7 +950,8 @@
|
||||
"timeout": "复制操作超时。请尝试使用较小的文件或检查您的连接。",
|
||||
"failed": "复制操作失败。请重试。",
|
||||
"aborted": "由于超时,复制操作已取消。"
|
||||
}
|
||||
},
|
||||
"invalidDate": "无效日期"
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
@@ -1010,7 +1166,10 @@
|
||||
},
|
||||
"searchBar": {
|
||||
"placeholder": "搜索文件...",
|
||||
"results": "共{total}个文件,找到{filtered}个"
|
||||
"results": "共{total}个文件,找到{filtered}个",
|
||||
"placeholderFolders": "搜索文件夹...",
|
||||
"noResults": "未找到 \"{query}\" 的搜索结果",
|
||||
"placeholderFiles": "搜索文件..."
|
||||
},
|
||||
"settings": {
|
||||
"groups": {
|
||||
@@ -1223,7 +1382,18 @@
|
||||
"descriptionPlaceholder": "输入描述(可选)",
|
||||
"descriptionLabel": "描述",
|
||||
"bulkDeleteConfirmation": "您确定要删除{count, plural, =1 {1个共享} other {#个共享}}吗?此操作无法撤销。",
|
||||
"bulkDeleteTitle": "删除选中的共享"
|
||||
"bulkDeleteTitle": "删除选中的共享",
|
||||
"addDescriptionPlaceholder": "添加描述...",
|
||||
"aliasLabel": "链接别名",
|
||||
"aliasPlaceholder": "输入自定义别名",
|
||||
"copyLink": "复制链接",
|
||||
"fileTitle": "分享文件",
|
||||
"folderTitle": "分享文件夹",
|
||||
"generateLink": "生成链接",
|
||||
"linkDescriptionFile": "生成自定义链接以分享文件",
|
||||
"linkDescriptionFolder": "生成自定义链接以分享文件夹",
|
||||
"linkReady": "您的分享链接已准备好:",
|
||||
"linkTitle": "生成链接"
|
||||
},
|
||||
"shareDetails": {
|
||||
"title": "共享详情",
|
||||
@@ -1350,7 +1520,8 @@
|
||||
"files": "文件",
|
||||
"totalSize": "总大小",
|
||||
"creating": "创建中...",
|
||||
"create": "创建分享"
|
||||
"create": "创建分享",
|
||||
"itemsToShare": "要分享的项目({count} 个项目)"
|
||||
},
|
||||
"shareSecurity": {
|
||||
"subtitle": "为此分享配置密码保护和安全选项",
|
||||
@@ -1455,7 +1626,8 @@
|
||||
"download": "选择下载"
|
||||
},
|
||||
"selectAll": "全选",
|
||||
"selectShare": "选择共享 {shareName}"
|
||||
"selectShare": "选择共享 {shareName}",
|
||||
"folderCount": "文件夹"
|
||||
},
|
||||
"storageUsage": {
|
||||
"title": "存储使用情况",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "palmr-web",
|
||||
"version": "3.1.8-beta",
|
||||
"version": "3.2.3-beta",
|
||||
"description": "Frontend for Palmr",
|
||||
"private": true,
|
||||
"author": "Daniel Luiz Alves <daniel@kyantech.com.br>",
|
||||
@@ -35,6 +35,7 @@
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.3",
|
||||
"@radix-ui/react-avatar": "^1.1.4",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-collapsible": "^1.1.12",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
@@ -99,4 +100,4 @@
|
||||
"tailwindcss": "4.1.11",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
}
|
4753
apps/web/pnpm-lock.yaml
generated
4753
apps/web/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -150,6 +150,18 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
|
||||
return fileName.split(".").pop() || "";
|
||||
};
|
||||
|
||||
const calculateUploadTimeout = (fileSize: number): number => {
|
||||
const baseTimeout = 300000;
|
||||
const fileSizeMB = fileSize / (1024 * 1024);
|
||||
if (fileSizeMB > 500) {
|
||||
const extraMB = fileSizeMB - 500;
|
||||
const extraMinutes = Math.ceil(extraMB / 100);
|
||||
return baseTimeout + extraMinutes * 60000;
|
||||
}
|
||||
|
||||
return baseTimeout;
|
||||
};
|
||||
|
||||
const uploadFileToStorage = async (
|
||||
file: File,
|
||||
presignedUrl: string,
|
||||
@@ -172,10 +184,14 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
|
||||
throw new Error(result.error || "Chunked upload failed");
|
||||
}
|
||||
} else {
|
||||
const uploadTimeout = calculateUploadTimeout(file.size);
|
||||
await axios.put(presignedUrl, file, {
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
},
|
||||
timeout: uploadTimeout,
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (onProgress && progressEvent.total) {
|
||||
const progress = (progressEvent.loaded / progressEvent.total) * 100;
|
||||
@@ -421,7 +437,7 @@ export function FileUploadSection({ reverseShare, password, alias, onUploadSucce
|
||||
);
|
||||
}}
|
||||
disabled={isUploading}
|
||||
title={t("reverseShares.upload.retry")}
|
||||
title={t("reverseShares.upload.errors.retry")}
|
||||
>
|
||||
<IconUpload className="h-4 w-4" />
|
||||
</Button>
|
||||
|
@@ -18,7 +18,7 @@ export function TransparentFooter() {
|
||||
title={t("footer.kyanHomepage")}
|
||||
>
|
||||
<span className="text-white/70 text-xs sm:text-sm">{t("footer.poweredBy")}</span>
|
||||
<p className="text-white text-xs sm:text-sm font-medium cursor-pointer hover:text-primary">
|
||||
<p className="text-primary text-xs sm:text-sm font-medium cursor-pointer hover:text-primary/80">
|
||||
Kyantech Solutions
|
||||
</p>
|
||||
</Link>
|
||||
|
@@ -120,10 +120,11 @@ function mapReverseShareToFormData(reverseShare: ReverseShare): EditReverseShare
|
||||
pageLayout: (reverseShare.pageLayout as "DEFAULT" | "WETRANSFER") || DEFAULT_VALUES.PAGE_LAYOUT,
|
||||
nameFieldRequired: (reverseShare.nameFieldRequired as "HIDDEN" | "OPTIONAL" | "REQUIRED") || "OPTIONAL",
|
||||
emailFieldRequired: (reverseShare.emailFieldRequired as "HIDDEN" | "OPTIONAL" | "REQUIRED") || "OPTIONAL",
|
||||
hasExpiration: false,
|
||||
hasFileLimits: false,
|
||||
hasFieldRequirements: false,
|
||||
hasPassword: false,
|
||||
hasExpiration: !!reverseShare.expiration,
|
||||
hasFileLimits: !!(reverseShare.maxFiles || reverseShare.maxFileSize || reverseShare.allowedFileTypes),
|
||||
hasFieldRequirements:
|
||||
reverseShare.nameFieldRequired !== "OPTIONAL" || reverseShare.emailFieldRequired !== "OPTIONAL",
|
||||
hasPassword: reverseShare.hasPassword,
|
||||
password: DEFAULT_VALUES.EMPTY_STRING,
|
||||
isActive: reverseShare.isActive,
|
||||
noFilesLimit: !reverseShare.maxFiles,
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user