mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-14 22:08:45 +00:00
파일 업로드 스케쥴링 구현
암호화는 동시에 최대 4개까지, 업로드는 1개까지 가능하도록 설정했습니다.
This commit is contained in:
@@ -27,6 +27,7 @@
|
|||||||
"@types/ms": "^0.7.34",
|
"@types/ms": "^0.7.34",
|
||||||
"@types/node-schedule": "^2.1.7",
|
"@types/node-schedule": "^2.1.7",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"dexie": "^4.0.10",
|
"dexie": "^4.0.10",
|
||||||
"drizzle-kit": "^0.22.8",
|
"drizzle-kit": "^0.22.8",
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
@@ -38,6 +39,7 @@
|
|||||||
"globals": "^15.14.0",
|
"globals": "^15.14.0",
|
||||||
"heic2any": "^0.0.4",
|
"heic2any": "^0.0.4",
|
||||||
"mime": "^4.0.6",
|
"mime": "^4.0.6",
|
||||||
|
"p-limit": "^6.2.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.3.2",
|
"prettier-plugin-svelte": "^3.3.2",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.9",
|
"prettier-plugin-tailwindcss": "^0.6.9",
|
||||||
|
|||||||
90
pnpm-lock.yaml
generated
90
pnpm-lock.yaml
generated
@@ -63,6 +63,9 @@ importers:
|
|||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.20
|
specifier: ^10.4.20
|
||||||
version: 10.4.20(postcss@8.4.49)
|
version: 10.4.20(postcss@8.4.49)
|
||||||
|
axios:
|
||||||
|
specifier: ^1.7.9
|
||||||
|
version: 1.7.9
|
||||||
dexie:
|
dexie:
|
||||||
specifier: ^4.0.10
|
specifier: ^4.0.10
|
||||||
version: 4.0.10
|
version: 4.0.10
|
||||||
@@ -96,6 +99,9 @@ importers:
|
|||||||
mime:
|
mime:
|
||||||
specifier: ^4.0.6
|
specifier: ^4.0.6
|
||||||
version: 4.0.6
|
version: 4.0.6
|
||||||
|
p-limit:
|
||||||
|
specifier: ^6.2.0
|
||||||
|
version: 6.2.0
|
||||||
prettier:
|
prettier:
|
||||||
specifier: ^3.4.2
|
specifier: ^3.4.2
|
||||||
version: 3.4.2
|
version: 3.4.2
|
||||||
@@ -975,6 +981,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
asynckit@0.4.0:
|
||||||
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
autoprefixer@10.4.20:
|
autoprefixer@10.4.20:
|
||||||
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
|
resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
@@ -982,6 +991,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
postcss: ^8.1.0
|
postcss: ^8.1.0
|
||||||
|
|
||||||
|
axios@1.7.9:
|
||||||
|
resolution: {integrity: sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==}
|
||||||
|
|
||||||
axobject-query@4.1.0:
|
axobject-query@4.1.0:
|
||||||
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -1063,6 +1075,10 @@ packages:
|
|||||||
color-name@1.1.4:
|
color-name@1.1.4:
|
||||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
commander@4.1.1:
|
commander@4.1.1:
|
||||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -1117,6 +1133,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||||
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
detect-libc@2.0.3:
|
detect-libc@2.0.3:
|
||||||
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -1412,10 +1432,23 @@ packages:
|
|||||||
flatted@3.3.2:
|
flatted@3.3.2:
|
||||||
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
|
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
|
||||||
|
|
||||||
|
follow-redirects@1.15.9:
|
||||||
|
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
||||||
|
engines: {node: '>=4.0'}
|
||||||
|
peerDependencies:
|
||||||
|
debug: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
debug:
|
||||||
|
optional: true
|
||||||
|
|
||||||
foreground-child@3.3.0:
|
foreground-child@3.3.0:
|
||||||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
form-data@4.0.1:
|
||||||
|
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
|
||||||
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
fraction.js@4.3.7:
|
fraction.js@4.3.7:
|
||||||
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
|
||||||
|
|
||||||
@@ -1619,6 +1652,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
|
||||||
engines: {node: '>=8.6'}
|
engines: {node: '>=8.6'}
|
||||||
|
|
||||||
|
mime-db@1.52.0:
|
||||||
|
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
|
||||||
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
mime@4.0.6:
|
mime@4.0.6:
|
||||||
resolution: {integrity: sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==}
|
resolution: {integrity: sha512-4rGt7rvQHBbaSOF9POGkk1ocRP16Md1x36Xma8sz8h8/vfCUI2OtEIeCqe4Ofes853x4xDoPiFLIT47J5fI/7A==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
@@ -1719,6 +1760,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
p-limit@6.2.0:
|
||||||
|
resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
p-locate@5.0.0:
|
p-locate@5.0.0:
|
||||||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1913,6 +1958,9 @@ packages:
|
|||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
proxy-from-env@1.1.0:
|
||||||
|
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==}
|
||||||
|
|
||||||
@@ -2263,6 +2311,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
|
yocto-queue@1.1.1:
|
||||||
|
resolution: {integrity: sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==}
|
||||||
|
engines: {node: '>=12.20'}
|
||||||
|
|
||||||
zimmerframe@1.1.2:
|
zimmerframe@1.1.2:
|
||||||
resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==}
|
resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==}
|
||||||
|
|
||||||
@@ -2919,6 +2971,8 @@ snapshots:
|
|||||||
|
|
||||||
aria-query@5.3.2: {}
|
aria-query@5.3.2: {}
|
||||||
|
|
||||||
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
autoprefixer@10.4.20(postcss@8.4.49):
|
autoprefixer@10.4.20(postcss@8.4.49):
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist: 4.24.4
|
browserslist: 4.24.4
|
||||||
@@ -2929,6 +2983,14 @@ snapshots:
|
|||||||
postcss: 8.4.49
|
postcss: 8.4.49
|
||||||
postcss-value-parser: 4.2.0
|
postcss-value-parser: 4.2.0
|
||||||
|
|
||||||
|
axios@1.7.9:
|
||||||
|
dependencies:
|
||||||
|
follow-redirects: 1.15.9
|
||||||
|
form-data: 4.0.1
|
||||||
|
proxy-from-env: 1.1.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- debug
|
||||||
|
|
||||||
axobject-query@4.1.0: {}
|
axobject-query@4.1.0: {}
|
||||||
|
|
||||||
balanced-match@1.0.2: {}
|
balanced-match@1.0.2: {}
|
||||||
@@ -3016,6 +3078,10 @@ snapshots:
|
|||||||
|
|
||||||
color-name@1.1.4: {}
|
color-name@1.1.4: {}
|
||||||
|
|
||||||
|
combined-stream@1.0.8:
|
||||||
|
dependencies:
|
||||||
|
delayed-stream: 1.0.0
|
||||||
|
|
||||||
commander@4.1.1: {}
|
commander@4.1.1: {}
|
||||||
|
|
||||||
commondir@1.0.1: {}
|
commondir@1.0.1: {}
|
||||||
@@ -3052,6 +3118,8 @@ snapshots:
|
|||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
|
delayed-stream@1.0.0: {}
|
||||||
|
|
||||||
detect-libc@2.0.3: {}
|
detect-libc@2.0.3: {}
|
||||||
|
|
||||||
devalue@5.1.1: {}
|
devalue@5.1.1: {}
|
||||||
@@ -3348,11 +3416,19 @@ snapshots:
|
|||||||
|
|
||||||
flatted@3.3.2: {}
|
flatted@3.3.2: {}
|
||||||
|
|
||||||
|
follow-redirects@1.15.9: {}
|
||||||
|
|
||||||
foreground-child@3.3.0:
|
foreground-child@3.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
|
|
||||||
|
form-data@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
asynckit: 0.4.0
|
||||||
|
combined-stream: 1.0.8
|
||||||
|
mime-types: 2.1.35
|
||||||
|
|
||||||
fraction.js@4.3.7: {}
|
fraction.js@4.3.7: {}
|
||||||
|
|
||||||
fs-constants@1.0.0: {}
|
fs-constants@1.0.0: {}
|
||||||
@@ -3519,6 +3595,12 @@ snapshots:
|
|||||||
braces: 3.0.3
|
braces: 3.0.3
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
mime-db@1.52.0: {}
|
||||||
|
|
||||||
|
mime-types@2.1.35:
|
||||||
|
dependencies:
|
||||||
|
mime-db: 1.52.0
|
||||||
|
|
||||||
mime@4.0.6: {}
|
mime@4.0.6: {}
|
||||||
|
|
||||||
mimic-response@3.1.0: {}
|
mimic-response@3.1.0: {}
|
||||||
@@ -3603,6 +3685,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yocto-queue: 0.1.0
|
yocto-queue: 0.1.0
|
||||||
|
|
||||||
|
p-limit@6.2.0:
|
||||||
|
dependencies:
|
||||||
|
yocto-queue: 1.1.1
|
||||||
|
|
||||||
p-locate@5.0.0:
|
p-locate@5.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-limit: 3.1.0
|
p-limit: 3.1.0
|
||||||
@@ -3726,6 +3812,8 @@ snapshots:
|
|||||||
|
|
||||||
prettier@3.4.2: {}
|
prettier@3.4.2: {}
|
||||||
|
|
||||||
|
proxy-from-env@1.1.0: {}
|
||||||
|
|
||||||
pump@3.0.2:
|
pump@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
end-of-stream: 1.4.4
|
end-of-stream: 1.4.4
|
||||||
@@ -4092,6 +4180,8 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
|
yocto-queue@1.1.1: {}
|
||||||
|
|
||||||
zimmerframe@1.1.2: {}
|
zimmerframe@1.1.2: {}
|
||||||
|
|
||||||
zod@3.24.1: {}
|
zod@3.24.1: {}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ClientInit } from "@sveltejs/kit";
|
import type { ClientInit } from "@sveltejs/kit";
|
||||||
import { getClientKey, getMasterKeys, getHmacSecrets } from "$lib/indexedDB";
|
import { getClientKey, getMasterKeys, getHmacSecrets } from "$lib/indexedDB";
|
||||||
import { prepareFileCache } from "$lib/modules/cache";
|
import { prepareFileCache } from "$lib/modules/file";
|
||||||
import { prepareOpfs } from "$lib/modules/opfs";
|
import { prepareOpfs } from "$lib/modules/opfs";
|
||||||
import { clientKeyStore, masterKeyStore, hmacSecretStore } from "$lib/stores";
|
import { clientKeyStore, masterKeyStore, hmacSecretStore } from "$lib/stores";
|
||||||
|
|
||||||
|
|||||||
3
src/lib/modules/file/index.ts
Normal file
3
src/lib/modules/file/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./cache";
|
||||||
|
export * from "./info";
|
||||||
|
export * from "./upload";
|
||||||
221
src/lib/modules/file/upload.ts
Normal file
221
src/lib/modules/file/upload.ts
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
import ExifReader from "exifreader";
|
||||||
|
import { limitFunction } from "p-limit";
|
||||||
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
import {
|
||||||
|
encodeToBase64,
|
||||||
|
generateDataKey,
|
||||||
|
wrapDataKey,
|
||||||
|
encryptData,
|
||||||
|
encryptString,
|
||||||
|
signMessageHmac,
|
||||||
|
} from "$lib/modules/crypto";
|
||||||
|
import type {
|
||||||
|
DuplicateFileScanRequest,
|
||||||
|
DuplicateFileScanResponse,
|
||||||
|
FileUploadRequest,
|
||||||
|
} from "$lib/server/schemas";
|
||||||
|
import {
|
||||||
|
fileUploadStatusStore,
|
||||||
|
type MasterKey,
|
||||||
|
type HmacSecret,
|
||||||
|
type FileUploadStatus,
|
||||||
|
} from "$lib/stores";
|
||||||
|
|
||||||
|
const requestDuplicateFileScan = limitFunction(
|
||||||
|
async (file: File, hmacSecret: HmacSecret, onDuplicate: () => Promise<boolean>) => {
|
||||||
|
const fileBuffer = await file.arrayBuffer();
|
||||||
|
const fileSigned = encodeToBase64(await signMessageHmac(fileBuffer, hmacSecret.secret));
|
||||||
|
|
||||||
|
const res = await axios.post("/api/file/scanDuplicates", {
|
||||||
|
hskVersion: hmacSecret.version,
|
||||||
|
contentHmac: fileSigned,
|
||||||
|
} satisfies DuplicateFileScanRequest);
|
||||||
|
const { files }: DuplicateFileScanResponse = res.data;
|
||||||
|
|
||||||
|
if (files.length === 0 || (await onDuplicate())) {
|
||||||
|
return { fileBuffer, fileSigned };
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ concurrency: 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFileType = (file: File) => {
|
||||||
|
if (file.type) return file.type;
|
||||||
|
if (file.name.endsWith(".heic")) return "image/heic";
|
||||||
|
throw new Error("Unknown file type");
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractExifDateTime = (fileBuffer: ArrayBuffer) => {
|
||||||
|
const exif = ExifReader.load(fileBuffer);
|
||||||
|
const dateTimeOriginal = exif["DateTimeOriginal"]?.description;
|
||||||
|
const offsetTimeOriginal = exif["OffsetTimeOriginal"]?.description;
|
||||||
|
if (!dateTimeOriginal) return undefined;
|
||||||
|
|
||||||
|
const [date, time] = dateTimeOriginal.split(" ");
|
||||||
|
if (!date || !time) return undefined;
|
||||||
|
|
||||||
|
const [year, month, day] = date.split(":").map(Number);
|
||||||
|
const [hour, minute, second] = time.split(":").map(Number);
|
||||||
|
if (!year || !month || !day || !hour || !minute || !second) return undefined;
|
||||||
|
|
||||||
|
if (!offsetTimeOriginal) {
|
||||||
|
// No timezone information.. Assume local timezone
|
||||||
|
return new Date(year, month - 1, day, hour, minute, second);
|
||||||
|
}
|
||||||
|
|
||||||
|
const offsetSign = offsetTimeOriginal[0] === "+" ? 1 : -1;
|
||||||
|
const [offsetHour, offsetMinute] = offsetTimeOriginal.slice(1).split(":").map(Number);
|
||||||
|
|
||||||
|
const utcDate = Date.UTC(year, month - 1, day, hour, minute, second);
|
||||||
|
const offsetMs = offsetSign * ((offsetHour ?? 0) * 60 + (offsetMinute ?? 0)) * 60 * 1000;
|
||||||
|
return new Date(utcDate - offsetMs);
|
||||||
|
};
|
||||||
|
|
||||||
|
const encryptFile = limitFunction(
|
||||||
|
async (
|
||||||
|
status: Writable<FileUploadStatus>,
|
||||||
|
file: File,
|
||||||
|
fileBuffer: ArrayBuffer,
|
||||||
|
masterKey: MasterKey,
|
||||||
|
) => {
|
||||||
|
status.update((value) => {
|
||||||
|
value.status = "encrypting";
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fileType = getFileType(file);
|
||||||
|
|
||||||
|
let createdAt;
|
||||||
|
if (fileType.startsWith("image/")) {
|
||||||
|
createdAt = extractExifDateTime(fileBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dataKey, dataKeyVersion } = await generateDataKey();
|
||||||
|
const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key);
|
||||||
|
|
||||||
|
const fileEncrypted = await encryptData(fileBuffer, dataKey);
|
||||||
|
const nameEncrypted = await encryptString(file.name, dataKey);
|
||||||
|
const createdAtEncrypted =
|
||||||
|
createdAt && (await encryptString(createdAt.getTime().toString(), dataKey));
|
||||||
|
const lastModifiedAtEncrypted = await encryptString(file.lastModified.toString(), dataKey);
|
||||||
|
|
||||||
|
status.update((value) => {
|
||||||
|
value.status = "upload-pending";
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
dataKeyWrapped,
|
||||||
|
dataKeyVersion,
|
||||||
|
fileEncrypted,
|
||||||
|
fileType,
|
||||||
|
nameEncrypted,
|
||||||
|
createdAtEncrypted,
|
||||||
|
lastModifiedAtEncrypted,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
{ concurrency: 4 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const uploadFileInternal = limitFunction(
|
||||||
|
async (status: Writable<FileUploadStatus>, form: FormData) => {
|
||||||
|
status.update((value) => {
|
||||||
|
value.status = "uploading";
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
await axios.post("/api/file/upload", form, {
|
||||||
|
onUploadProgress: ({ progress, rate, estimated }) => {
|
||||||
|
status.update((value) => {
|
||||||
|
value.progress = progress;
|
||||||
|
value.rate = rate;
|
||||||
|
value.estimated = estimated;
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
status.update((value) => {
|
||||||
|
value.status = "uploaded";
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{ concurrency: 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
export const uploadFile = async (
|
||||||
|
file: File,
|
||||||
|
parentId: "root" | number,
|
||||||
|
hmacSecret: HmacSecret,
|
||||||
|
masterKey: MasterKey,
|
||||||
|
onDuplicate: () => Promise<boolean>,
|
||||||
|
) => {
|
||||||
|
const status = writable<FileUploadStatus>({
|
||||||
|
name: file.name,
|
||||||
|
parentId,
|
||||||
|
status: "encryption-pending",
|
||||||
|
});
|
||||||
|
fileUploadStatusStore.update((value) => {
|
||||||
|
value.push(status);
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { fileBuffer, fileSigned } = await requestDuplicateFileScan(
|
||||||
|
file,
|
||||||
|
hmacSecret,
|
||||||
|
onDuplicate,
|
||||||
|
);
|
||||||
|
if (!fileBuffer || !fileSigned) {
|
||||||
|
status.update((value) => {
|
||||||
|
value.status = "canceled";
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
dataKeyWrapped,
|
||||||
|
dataKeyVersion,
|
||||||
|
fileEncrypted,
|
||||||
|
fileType,
|
||||||
|
nameEncrypted,
|
||||||
|
createdAtEncrypted,
|
||||||
|
lastModifiedAtEncrypted,
|
||||||
|
} = await encryptFile(status, file, fileBuffer, masterKey);
|
||||||
|
|
||||||
|
const form = new FormData();
|
||||||
|
form.set(
|
||||||
|
"metadata",
|
||||||
|
JSON.stringify({
|
||||||
|
parentId,
|
||||||
|
mekVersion: masterKey.version,
|
||||||
|
dek: dataKeyWrapped,
|
||||||
|
dekVersion: dataKeyVersion.toISOString(),
|
||||||
|
hskVersion: hmacSecret.version,
|
||||||
|
contentHmac: fileSigned,
|
||||||
|
contentType: fileType,
|
||||||
|
contentIv: fileEncrypted.iv,
|
||||||
|
name: nameEncrypted.ciphertext,
|
||||||
|
nameIv: nameEncrypted.iv,
|
||||||
|
createdAt: createdAtEncrypted?.ciphertext,
|
||||||
|
createdAtIv: createdAtEncrypted?.iv,
|
||||||
|
lastModifiedAt: lastModifiedAtEncrypted.ciphertext,
|
||||||
|
lastModifiedAtIv: lastModifiedAtEncrypted.iv,
|
||||||
|
} as FileUploadRequest),
|
||||||
|
);
|
||||||
|
form.set("content", new Blob([fileEncrypted.ciphertext]));
|
||||||
|
|
||||||
|
await uploadFileInternal(status, form);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
status.update((value) => {
|
||||||
|
value.status = "error";
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Writable } from "svelte/store";
|
import { writable, type Writable } from "svelte/store";
|
||||||
|
|
||||||
export type DirectoryInfo =
|
export type DirectoryInfo =
|
||||||
| {
|
| {
|
||||||
@@ -29,6 +29,24 @@ export interface FileInfo {
|
|||||||
lastModifiedAt: Date;
|
lastModifiedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileUploadStatus {
|
||||||
|
name: string;
|
||||||
|
parentId: "root" | number;
|
||||||
|
status:
|
||||||
|
| "encryption-pending"
|
||||||
|
| "encrypting"
|
||||||
|
| "upload-pending"
|
||||||
|
| "uploading"
|
||||||
|
| "uploaded"
|
||||||
|
| "canceled"
|
||||||
|
| "error";
|
||||||
|
progress?: number;
|
||||||
|
rate?: number;
|
||||||
|
estimated?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export const directoryInfoStore = new Map<"root" | number, Writable<DirectoryInfo | null>>();
|
export const directoryInfoStore = new Map<"root" | number, Writable<DirectoryInfo | null>>();
|
||||||
|
|
||||||
export const fileInfoStore = new Map<number, Writable<FileInfo | null>>();
|
export const fileInfoStore = new Map<number, Writable<FileInfo | null>>();
|
||||||
|
|
||||||
|
export const fileUploadStatusStore = writable<Writable<FileUploadStatus>[]>([]);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getFileCache, storeFileCache } from "$lib/modules/cache";
|
import { getFileCache, storeFileCache } from "$lib/modules/file";
|
||||||
import { decryptData } from "$lib/modules/crypto";
|
import { decryptData } from "$lib/modules/crypto";
|
||||||
|
|
||||||
export const requestFileDownload = async (
|
export const requestFileDownload = async (
|
||||||
|
|||||||
@@ -3,8 +3,7 @@
|
|||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
import { TopBar } from "$lib/components";
|
import { TopBar } from "$lib/components";
|
||||||
import type { FileCacheIndex } from "$lib/indexedDB";
|
import type { FileCacheIndex } from "$lib/indexedDB";
|
||||||
import { getFileCacheIndex } from "$lib/modules/cache";
|
import { getFileCacheIndex, getFileInfo } from "$lib/modules/file";
|
||||||
import { getFileInfo } from "$lib/modules/file";
|
|
||||||
import { masterKeyStore, type FileInfo } from "$lib/stores";
|
import { masterKeyStore, type FileInfo } from "$lib/stores";
|
||||||
import File from "./File.svelte";
|
import File from "./File.svelte";
|
||||||
import { formatFileSize, deleteFileCache as doDeleteFileCache } from "./service";
|
import { formatFileSize, deleteFileCache as doDeleteFileCache } from "./service";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { deleteFileCache as doDeleteFileCache } from "$lib/modules/cache";
|
import { deleteFileCache as doDeleteFileCache } from "$lib/modules/file";
|
||||||
|
|
||||||
export { formatDate, formatFileSize } from "$lib/modules/util";
|
export { formatDate, formatFileSize } from "$lib/modules/util";
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
import {
|
import {
|
||||||
requestHmacSecretDownload,
|
requestHmacSecretDownload,
|
||||||
requestDirectoryCreation,
|
requestDirectoryCreation,
|
||||||
requestDuplicateFileScan,
|
|
||||||
requestFileUpload,
|
requestFileUpload,
|
||||||
requestDirectoryEntryRename,
|
requestDirectoryEntryRename,
|
||||||
requestDirectoryEntryDeletion,
|
requestDirectoryEntryDeletion,
|
||||||
@@ -25,17 +24,11 @@
|
|||||||
|
|
||||||
import IconAdd from "~icons/material-symbols/add";
|
import IconAdd from "~icons/material-symbols/add";
|
||||||
|
|
||||||
interface LoadedFile {
|
|
||||||
file: File;
|
|
||||||
fileBuffer: ArrayBuffer;
|
|
||||||
fileSigned: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
let info: Writable<DirectoryInfo | null> | undefined = $state();
|
let info: Writable<DirectoryInfo | null> | undefined = $state();
|
||||||
let fileInput: HTMLInputElement | undefined = $state();
|
let fileInput: HTMLInputElement | undefined = $state();
|
||||||
let loadedFile: LoadedFile | undefined = $state();
|
let resolveForDuplicateFileModal: ((res: boolean) => void) | undefined = $state();
|
||||||
let selectedEntry: SelectedDirectoryEntry | undefined = $state();
|
let selectedEntry: SelectedDirectoryEntry | undefined = $state();
|
||||||
|
|
||||||
let isCreateBottomSheetOpen = $state(false);
|
let isCreateBottomSheetOpen = $state(false);
|
||||||
@@ -52,43 +45,32 @@
|
|||||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFile = (loadedFile: LoadedFile) => {
|
const uploadFile = () => {
|
||||||
requestFileUpload(
|
const file = fileInput?.files?.[0];
|
||||||
loadedFile.file,
|
if (!file) return;
|
||||||
loadedFile.fileBuffer,
|
|
||||||
loadedFile.fileSigned,
|
fileInput!.value = "";
|
||||||
data.id,
|
|
||||||
$masterKeyStore?.get(1)!,
|
requestFileUpload(file, data.id, $hmacSecretStore?.get(1)!, $masterKeyStore?.get(1)!, () => {
|
||||||
$hmacSecretStore?.get(1)!,
|
return new Promise((resolve) => {
|
||||||
)
|
resolveForDuplicateFileModal = resolve;
|
||||||
.then(() => {
|
isDuplicateFileModalOpen = true;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res) return;
|
||||||
|
|
||||||
// TODO: FIXME
|
// TODO: FIXME
|
||||||
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
|
||||||
window.alert("파일이 업로드되었어요.");
|
window.alert("파일이 업로드되었어요.");
|
||||||
})
|
})
|
||||||
.catch((e: Error) => {
|
.catch((e: Error) => {
|
||||||
// TODO: FIXME
|
// TODO: FIXME
|
||||||
|
console.error(e);
|
||||||
window.alert(`파일 업로드에 실패했어요.\n${e.message}`);
|
window.alert(`파일 업로드에 실패했어요.\n${e.message}`);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadAndUploadFile = async () => {
|
|
||||||
const file = fileInput?.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
fileInput!.value = "";
|
|
||||||
|
|
||||||
const scanRes = await requestDuplicateFileScan(file, $hmacSecretStore?.get(1)!);
|
|
||||||
if (scanRes === null) {
|
|
||||||
throw new Error("Failed to scan duplicate files");
|
|
||||||
} else if (scanRes.isDuplicate) {
|
|
||||||
loadedFile = { ...scanRes, file };
|
|
||||||
isDuplicateFileModalOpen = true;
|
|
||||||
} else {
|
|
||||||
uploadFile({ ...scanRes, file });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
if (!$hmacSecretStore && !(await requestHmacSecretDownload($masterKeyStore?.get(1)?.key!))) {
|
if (!$hmacSecretStore && !(await requestHmacSecretDownload($masterKeyStore?.get(1)?.key!))) {
|
||||||
throw new Error("Failed to download hmac secrets");
|
throw new Error("Failed to download hmac secrets");
|
||||||
@@ -104,7 +86,7 @@
|
|||||||
<title>파일</title>
|
<title>파일</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<input bind:this={fileInput} onchange={loadAndUploadFile} type="file" class="hidden" />
|
<input bind:this={fileInput} onchange={uploadFile} type="file" class="hidden" />
|
||||||
|
|
||||||
<div class="flex min-h-full flex-col px-4">
|
<div class="flex min-h-full flex-col px-4">
|
||||||
{#if data.id !== "root"}
|
{#if data.id !== "root"}
|
||||||
@@ -148,13 +130,14 @@
|
|||||||
<DuplicateFileModal
|
<DuplicateFileModal
|
||||||
bind:isOpen={isDuplicateFileModalOpen}
|
bind:isOpen={isDuplicateFileModalOpen}
|
||||||
onclose={() => {
|
onclose={() => {
|
||||||
|
resolveForDuplicateFileModal?.(false);
|
||||||
|
resolveForDuplicateFileModal = undefined;
|
||||||
isDuplicateFileModalOpen = false;
|
isDuplicateFileModalOpen = false;
|
||||||
loadedFile = undefined;
|
|
||||||
}}
|
}}
|
||||||
onDuplicateClick={() => {
|
onDuplicateClick={() => {
|
||||||
uploadFile(loadedFile!);
|
resolveForDuplicateFileModal?.(true);
|
||||||
|
resolveForDuplicateFileModal = undefined;
|
||||||
isDuplicateFileModalOpen = false;
|
isDuplicateFileModalOpen = false;
|
||||||
loadedFile = undefined;
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,7 @@
|
|||||||
<p>그래도 업로드할까요?</p>
|
<p>그래도 업로드할까요?</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button
|
<Button color="gray" onclick={onclose}>아니요</Button>
|
||||||
color="gray"
|
|
||||||
onclick={() => {
|
|
||||||
isOpen = false;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
아니요
|
|
||||||
</Button>
|
|
||||||
<Button onclick={onDuplicateClick}>업로드할게요</Button>
|
<Button onclick={onDuplicateClick}>업로드할게요</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,24 +1,12 @@
|
|||||||
import ExifReader from "exifreader";
|
|
||||||
import { callGetApi, callPostApi } from "$lib/hooks";
|
import { callGetApi, callPostApi } from "$lib/hooks";
|
||||||
import { storeHmacSecrets } from "$lib/indexedDB";
|
import { storeHmacSecrets } from "$lib/indexedDB";
|
||||||
import { deleteFileCache } from "$lib/modules/cache";
|
import { deleteFileCache, uploadFile } from "$lib/modules/file";
|
||||||
import {
|
import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto";
|
||||||
encodeToBase64,
|
|
||||||
generateDataKey,
|
|
||||||
wrapDataKey,
|
|
||||||
unwrapHmacSecret,
|
|
||||||
encryptData,
|
|
||||||
encryptString,
|
|
||||||
signMessageHmac,
|
|
||||||
} from "$lib/modules/crypto";
|
|
||||||
import type {
|
import type {
|
||||||
DirectoryRenameRequest,
|
DirectoryRenameRequest,
|
||||||
DirectoryCreateRequest,
|
DirectoryCreateRequest,
|
||||||
FileRenameRequest,
|
FileRenameRequest,
|
||||||
FileUploadRequest,
|
|
||||||
HmacSecretListResponse,
|
HmacSecretListResponse,
|
||||||
DuplicateFileScanRequest,
|
|
||||||
DuplicateFileScanResponse,
|
|
||||||
DirectoryDeleteResponse,
|
DirectoryDeleteResponse,
|
||||||
} from "$lib/server/schemas";
|
} from "$lib/server/schemas";
|
||||||
import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
|
import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
|
||||||
@@ -68,106 +56,14 @@ export const requestDirectoryCreation = async (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestDuplicateFileScan = async (file: File, hmacSecret: HmacSecret) => {
|
|
||||||
const fileBuffer = await file.arrayBuffer();
|
|
||||||
const fileSigned = encodeToBase64(await signMessageHmac(fileBuffer, hmacSecret.secret));
|
|
||||||
const res = await callPostApi<DuplicateFileScanRequest>("/api/file/scanDuplicates", {
|
|
||||||
hskVersion: hmacSecret.version,
|
|
||||||
contentHmac: fileSigned,
|
|
||||||
});
|
|
||||||
if (!res.ok) return null;
|
|
||||||
|
|
||||||
const { files }: DuplicateFileScanResponse = await res.json();
|
|
||||||
return {
|
|
||||||
fileBuffer,
|
|
||||||
fileSigned,
|
|
||||||
isDuplicate: files.length > 0,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const extractExifDateTime = (fileBuffer: ArrayBuffer) => {
|
|
||||||
const exif = ExifReader.load(fileBuffer);
|
|
||||||
const dateTimeOriginal = exif["DateTimeOriginal"]?.description;
|
|
||||||
const offsetTimeOriginal = exif["OffsetTimeOriginal"]?.description;
|
|
||||||
if (!dateTimeOriginal) return undefined;
|
|
||||||
|
|
||||||
const [date, time] = dateTimeOriginal.split(" ");
|
|
||||||
if (!date || !time) return undefined;
|
|
||||||
|
|
||||||
const [year, month, day] = date.split(":").map(Number);
|
|
||||||
const [hour, minute, second] = time.split(":").map(Number);
|
|
||||||
if (!year || !month || !day || !hour || !minute || !second) return undefined;
|
|
||||||
|
|
||||||
if (!offsetTimeOriginal) {
|
|
||||||
// No timezone information -> Local timezone
|
|
||||||
return new Date(year, month - 1, day, hour, minute, second);
|
|
||||||
}
|
|
||||||
|
|
||||||
const offsetSign = offsetTimeOriginal[0] === "+" ? 1 : -1;
|
|
||||||
const [offsetHour, offsetMinute] = offsetTimeOriginal.slice(1).split(":").map(Number);
|
|
||||||
|
|
||||||
const utcDate = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
||||||
const offsetMs = offsetSign * ((offsetHour ?? 0) * 60 + (offsetMinute ?? 0)) * 60 * 1000;
|
|
||||||
return new Date(utcDate - offsetMs);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requestFileUpload = async (
|
export const requestFileUpload = async (
|
||||||
file: File,
|
file: File,
|
||||||
fileBuffer: ArrayBuffer,
|
|
||||||
fileSigned: string,
|
|
||||||
parentId: "root" | number,
|
parentId: "root" | number,
|
||||||
masterKey: MasterKey,
|
|
||||||
hmacSecret: HmacSecret,
|
hmacSecret: HmacSecret,
|
||||||
|
masterKey: MasterKey,
|
||||||
|
onDuplicate: () => Promise<boolean>,
|
||||||
) => {
|
) => {
|
||||||
let createdAt = undefined;
|
return await uploadFile(file, parentId, hmacSecret, masterKey, onDuplicate);
|
||||||
if (file.type.startsWith("image/")) {
|
|
||||||
createdAt = extractExifDateTime(fileBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { dataKey, dataKeyVersion } = await generateDataKey();
|
|
||||||
const fileEncrypted = await encryptData(fileBuffer, dataKey);
|
|
||||||
const nameEncrypted = await encryptString(file.name, dataKey);
|
|
||||||
const createdAtEncrypted =
|
|
||||||
createdAt && (await encryptString(createdAt.getTime().toString(), dataKey));
|
|
||||||
const lastModifiedAtEncrypted = await encryptString(file.lastModified.toString(), dataKey);
|
|
||||||
|
|
||||||
const form = new FormData();
|
|
||||||
form.set(
|
|
||||||
"metadata",
|
|
||||||
JSON.stringify({
|
|
||||||
parentId,
|
|
||||||
mekVersion: masterKey.version,
|
|
||||||
dek: await wrapDataKey(dataKey, masterKey.key),
|
|
||||||
dekVersion: dataKeyVersion.toISOString(),
|
|
||||||
hskVersion: hmacSecret.version,
|
|
||||||
contentHmac: fileSigned,
|
|
||||||
contentType: file.type,
|
|
||||||
contentIv: fileEncrypted.iv,
|
|
||||||
name: nameEncrypted.ciphertext,
|
|
||||||
nameIv: nameEncrypted.iv,
|
|
||||||
createdAt: createdAtEncrypted?.ciphertext,
|
|
||||||
createdAtIv: createdAtEncrypted?.iv,
|
|
||||||
lastModifiedAt: lastModifiedAtEncrypted.ciphertext,
|
|
||||||
lastModifiedAtIv: lastModifiedAtEncrypted.iv,
|
|
||||||
} satisfies FileUploadRequest),
|
|
||||||
);
|
|
||||||
form.set("content", new Blob([fileEncrypted.ciphertext]));
|
|
||||||
|
|
||||||
return new Promise<void>((resolve, reject) => {
|
|
||||||
// TODO: Progress, Scheduling, ...
|
|
||||||
|
|
||||||
const xhr = new XMLHttpRequest();
|
|
||||||
xhr.addEventListener("load", () => {
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(xhr.responseText));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xhr.open("POST", "/api/file/upload");
|
|
||||||
xhr.send(form);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const requestDirectoryEntryRename = async (
|
export const requestDirectoryEntryRename = async (
|
||||||
|
|||||||
Reference in New Issue
Block a user