@brftech/filex-core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ArchiveViewer-EGgNqLPE.js → ArchiveViewer-w6GDJJt9.js} +2 -2
- package/dist/{ArchiveViewer-EGgNqLPE.js.map → ArchiveViewer-w6GDJJt9.js.map} +1 -1
- package/dist/{CsvViewer-BvriJ42M.js → CsvViewer-Ci5-mke8.js} +2 -2
- package/dist/{CsvViewer-BvriJ42M.js.map → CsvViewer-Ci5-mke8.js.map} +1 -1
- package/dist/{DrawioViewer-B_bSPsWX.js → DrawioViewer-D2iMuQfD.js} +2 -2
- package/dist/{DrawioViewer-B_bSPsWX.js.map → DrawioViewer-D2iMuQfD.js.map} +1 -1
- package/dist/{EpubViewer-hl68h4gG.js → EpubViewer-D4ydsa9X.js} +2 -2
- package/dist/{EpubViewer-hl68h4gG.js.map → EpubViewer-D4ydsa9X.js.map} +1 -1
- package/dist/{IpynbViewer-lhwOjXK7.js → IpynbViewer-CWBwiFKX.js} +2 -2
- package/dist/{IpynbViewer-lhwOjXK7.js.map → IpynbViewer-CWBwiFKX.js.map} +1 -1
- package/dist/{MermaidViewer-H2Z6AYXg.js → MermaidViewer-CDhuqQli.js} +2 -2
- package/dist/{MermaidViewer-H2Z6AYXg.js.map → MermaidViewer-CDhuqQli.js.map} +1 -1
- package/dist/{PsdViewer-Bsk-11yH.js → PsdViewer-C_TkFb2H.js} +2 -2
- package/dist/{PsdViewer-Bsk-11yH.js.map → PsdViewer-C_TkFb2H.js.map} +1 -1
- package/dist/{TiffViewer-D-j9vycG.js → TiffViewer-BmWCXR7e.js} +2 -2
- package/dist/{TiffViewer-D-j9vycG.js.map → TiffViewer-BmWCXR7e.js.map} +1 -1
- package/dist/{Viewer3D-Bcr70rTe.js → Viewer3D-5kPT1QsY.js} +2 -2
- package/dist/{Viewer3D-Bcr70rTe.js.map → Viewer3D-5kPT1QsY.js.map} +1 -1
- package/dist/filex-core.js +66 -50
- package/dist/filex-core.umd.cjs +53 -53
- package/dist/filex-core.umd.cjs.map +1 -1
- package/dist/index-nc0-oI_l.js +11723 -0
- package/dist/index-nc0-oI_l.js.map +1 -0
- package/dist/index.d.ts +130 -0
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/FileExplorer.vue +389 -9
- package/src/components/EncryptedFolderModal.vue +125 -0
- package/src/components/GalleryView.vue +1 -0
- package/src/components/GridView.vue +1 -0
- package/src/components/ListView.vue +1 -0
- package/src/composables/useFileApi.ts +7 -0
- package/src/index.ts +21 -0
- package/src/lib/e2ecrypto.ts +299 -0
- package/src/locales/en.ts +34 -0
- package/src/locales/tr.ts +34 -0
- package/src/modals/NewFolderModal.vue +18 -0
- package/src/modals/PreviewModal.vue +7 -2
- package/src/styles/base.css +93 -0
- package/src/types/FileNode.ts +3 -0
- package/dist/index-C_ZSnk_I.js +0 -11109
- package/dist/index-C_ZSnk_I.js.map +0 -1
package/src/FileExplorer.vue
CHANGED
|
@@ -71,6 +71,20 @@ import TabBar from './components/TabBar.vue';
|
|
|
71
71
|
import SecondaryPane from './components/SecondaryPane.vue';
|
|
72
72
|
import { useTabs, type TabState } from './composables/useTabs';
|
|
73
73
|
/* /wiring:d1 */
|
|
74
|
+
/* wiring:e2 — uçtan uca şifreli klasörler (docs/E2E-ENCRYPTION.md) */
|
|
75
|
+
import EncryptedFolderModal from './components/EncryptedFolderModal.vue';
|
|
76
|
+
import {
|
|
77
|
+
createKeyRing,
|
|
78
|
+
createMarker,
|
|
79
|
+
parseMarker,
|
|
80
|
+
verifyPassword,
|
|
81
|
+
encryptFile,
|
|
82
|
+
decryptFile,
|
|
83
|
+
hasMagic,
|
|
84
|
+
E2E_MARKER_NAME,
|
|
85
|
+
E2E_MAX_FILE_BYTES,
|
|
86
|
+
} from './lib/e2ecrypto';
|
|
87
|
+
/* /wiring:e2 */
|
|
74
88
|
|
|
75
89
|
import NewFolderModal from './modals/NewFolderModal.vue';
|
|
76
90
|
import RenameModal from './modals/RenameModal.vue';
|
|
@@ -671,6 +685,7 @@ async function load(path?: string) {
|
|
|
671
685
|
currentPath.value = '';
|
|
672
686
|
adapter.value = '';
|
|
673
687
|
dirname.value = '';
|
|
688
|
+
e2eRoot.value = ''; /* wiring:e2 — sanal kökte kilit ekranı olmaz */
|
|
674
689
|
files.value = virtualStorageRows();
|
|
675
690
|
return;
|
|
676
691
|
}
|
|
@@ -685,11 +700,16 @@ async function load(path?: string) {
|
|
|
685
700
|
adapter.value = resp.adapter;
|
|
686
701
|
dirname.value = resp.dirname;
|
|
687
702
|
dirPerm.value = (resp.perm as string) || '';
|
|
703
|
+
/* wiring:e2 — backend tells us when this dir sits inside an encrypted
|
|
704
|
+
subtree; '' resets on every plain folder. Drives the lock screen. */
|
|
705
|
+
e2eRoot.value = typeof resp.e2e_root === 'string' ? resp.e2e_root : '';
|
|
706
|
+
/* /wiring:e2 */
|
|
688
707
|
files.value = (resp.files || []).filter((f) => {
|
|
689
708
|
if (f.path.includes('.thumbs')) return false;
|
|
690
709
|
if (f.path.includes('.versions') || f.basename === '.versions') return false;
|
|
691
710
|
if (f.basename === '.trash') return false;
|
|
692
711
|
if (f.basename === '.keepdir') return false;
|
|
712
|
+
if (f.basename === E2E_MARKER_NAME) return false; /* wiring:e2 — marker gizli */
|
|
693
713
|
return true;
|
|
694
714
|
});
|
|
695
715
|
// Inject virtual `.trash` entry at root only.
|
|
@@ -725,6 +745,7 @@ async function load(path?: string) {
|
|
|
725
745
|
// show the dedicated not-found state instead of a toast over a stale
|
|
726
746
|
// listing that reads as "this folder is empty".
|
|
727
747
|
notFoundPath.value = String(requested);
|
|
748
|
+
e2eRoot.value = ''; /* wiring:e2 — ölü linkte kilit ekranı kalmasın */
|
|
728
749
|
files.value = [];
|
|
729
750
|
emit('error', { message: e, context: { path } });
|
|
730
751
|
return;
|
|
@@ -791,6 +812,7 @@ async function loadTrash() {
|
|
|
791
812
|
loading.value = true;
|
|
792
813
|
trashOrigin.value = adapter.value || '';
|
|
793
814
|
trashMode.value = true;
|
|
815
|
+
e2eRoot.value = ''; /* wiring:e2 — çöp görünümü şifreli bağlamın dışındadır */
|
|
794
816
|
selection.clear();
|
|
795
817
|
try {
|
|
796
818
|
const { entries } = await api.listTrash();
|
|
@@ -1173,6 +1195,15 @@ function openNode(n: FileNode) {
|
|
|
1173
1195
|
void load(target);
|
|
1174
1196
|
return;
|
|
1175
1197
|
}
|
|
1198
|
+
/* wiring:e2 — şifreli klasörde dosya açma: kilit açıksa çöz + salt-okunur
|
|
1199
|
+
önizleme (blob URL mevcut viewer'lara gider); kilitliyken hiçbir şey
|
|
1200
|
+
açılmaz (kilit ekranı zaten listeyi kapatır). */
|
|
1201
|
+
if (e2eUnlocked.value && n.type === 'file') {
|
|
1202
|
+
void e2eOpenPreview(n);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (e2eLocked.value && n.type === 'file') return;
|
|
1206
|
+
/* /wiring:e2 */
|
|
1176
1207
|
// "Aç" / double-click contract: open in a new tab against the
|
|
1177
1208
|
// standalone editor route, regardless of file type. The editor page
|
|
1178
1209
|
// picks the right viewer (OnlyOffice for office, Monaco for code/
|
|
@@ -1248,6 +1279,12 @@ async function restoreSelection(targets?: FileNode[]) {
|
|
|
1248
1279
|
}
|
|
1249
1280
|
|
|
1250
1281
|
function previewNode(n: FileNode) {
|
|
1282
|
+
/* wiring:e2 — önizleme de çözülmüş blob'dan beslenir */
|
|
1283
|
+
if (e2eUnlocked.value && n.type === 'file') {
|
|
1284
|
+
void e2eOpenPreview(n);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
/* /wiring:e2 */
|
|
1251
1288
|
previewMode.value = 'view';
|
|
1252
1289
|
previewTarget.value = n;
|
|
1253
1290
|
showPreview.value = true;
|
|
@@ -1269,6 +1306,13 @@ function openNodeInNewTab(n: FileNode) {
|
|
|
1269
1306
|
void load(target);
|
|
1270
1307
|
return;
|
|
1271
1308
|
}
|
|
1309
|
+
/* wiring:e2 — standalone editör rotası sunucudan HAM (şifreli) baytı
|
|
1310
|
+
çeker; şifreli klasörde her "Aç" in-page çözülmüş önizlemeye iner. */
|
|
1311
|
+
if (e2eActive.value) {
|
|
1312
|
+
if (e2eUnlocked.value) void e2eOpenPreview(n);
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
/* /wiring:e2 */
|
|
1272
1316
|
// RBAC: a viewer (no edit on this item) can't use the editable "Aç"
|
|
1273
1317
|
// surface — drop to the read-only in-page preview instead.
|
|
1274
1318
|
if (!permCanEdit((n.perm as string) ?? dirPerm.value)) {
|
|
@@ -1428,8 +1472,8 @@ function selectionActionList(sel: FileNode[]): ContextAction[] {
|
|
|
1428
1472
|
{ key: 'open-tab', label: t('ctx.open_new_tab'), icon: '⧉', hidden: !single || sel[0]?.type !== 'dir' } /* wiring:d1 — klasörü yeni sekmede aç */,
|
|
1429
1473
|
{ key: 'preview', label: t('ctx.preview'), icon: '👁', hidden: !single, disabled: !isFile },
|
|
1430
1474
|
{ key: 'download', label: t('ctx.download'), icon: '⬇', hidden: !single, disabled: !isFile },
|
|
1431
|
-
{ key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w
|
|
1432
|
-
{ key: 'access', label: accessLabel, icon: '🔗', hidden: !single || !w },
|
|
1475
|
+
{ key: 'convert', label: t('ctx.convert'), icon: '🔄', hidden: !single || !effectiveConvertUrl.value || !w || e2eActive.value /* wiring:e2 — convert ciphertext'e anlamsız */, disabled: !isFile },
|
|
1476
|
+
{ key: 'access', label: accessLabel, icon: '🔗', hidden: !single || !w || e2eActive.value /* wiring:e2 — paylaşım MVP'de kapalı (link ciphertext verir) */ },
|
|
1433
1477
|
{ key: 'details', label: t('ctx.details'), icon: 'ℹ', hidden: !any } /* koru:k1 */,
|
|
1434
1478
|
{ key: 'copy-id', label: copyIdLabel, icon: '🆔', hidden: !singleHasId, disabled: !singleHasId },
|
|
1435
1479
|
{ divider: true, key: 'sep1', label: '', hidden: !w },
|
|
@@ -1611,6 +1655,13 @@ async function duplicate(n: FileNode) {
|
|
|
1611
1655
|
}
|
|
1612
1656
|
|
|
1613
1657
|
function downloadFile(n: FileNode) {
|
|
1658
|
+
/* wiring:e2 — şifreli klasörde indirme: baytları çek, çöz, orijinal adla
|
|
1659
|
+
kaydet (ham ciphertext'i kullanıcıya vermek anlamsız). */
|
|
1660
|
+
if (e2eUnlocked.value) {
|
|
1661
|
+
void e2eDownload(n);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
/* /wiring:e2 */
|
|
1614
1665
|
// Keep `<adapter>://<rel>` so backend resolves the right storage
|
|
1615
1666
|
// (stripping it would default to the first storage, which 404s for
|
|
1616
1667
|
// any non-default storage like S3/SFTP/WebDAV).
|
|
@@ -1763,6 +1814,18 @@ function onFilePicked(ev: Event) {
|
|
|
1763
1814
|
|
|
1764
1815
|
async function uploadFiles(list: File[]) {
|
|
1765
1816
|
if (list.length === 0) return;
|
|
1817
|
+
/* wiring:e2 — şifreli klasörde upload şeffaf şifrelenir. Kilitliyken
|
|
1818
|
+
yükleme yok (düz metin sızdırma kapısı olurdu); 200MB üstü MVP
|
|
1819
|
+
tek-shot sınırına takılır ve uyarıyla atlanır. */
|
|
1820
|
+
if (e2eLocked.value) {
|
|
1821
|
+
flashToast(t('e2e.upload.locked'));
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
if (e2eUnlocked.value) {
|
|
1825
|
+
list = await e2eEncryptUploads(list);
|
|
1826
|
+
if (list.length === 0) return;
|
|
1827
|
+
}
|
|
1828
|
+
/* /wiring:e2 */
|
|
1766
1829
|
const canChunk = !!(api.endpoints.uploadInit && api.endpoints.uploadFinalize);
|
|
1767
1830
|
for (const f of list) {
|
|
1768
1831
|
// Chunked (S3 multipart) only when the endpoints exist AND the file is
|
|
@@ -2169,6 +2232,23 @@ function quickLookToggle() {
|
|
|
2169
2232
|
const sel = selection.nodes.value;
|
|
2170
2233
|
const n = sel.length === 1 ? sel[0] : null;
|
|
2171
2234
|
if (!n || n.type !== 'file' || n.basename === '.trash') return;
|
|
2235
|
+
/* wiring:e2 — Space peek şifreli klasörde önce çözer, sonra açar */
|
|
2236
|
+
if (e2eActive.value) {
|
|
2237
|
+
if (!e2eUnlocked.value) return;
|
|
2238
|
+
void (async () => {
|
|
2239
|
+
try {
|
|
2240
|
+
await e2eFetchDecrypted(n);
|
|
2241
|
+
} catch {
|
|
2242
|
+
flashToast(t('e2e.decrypt_failed'));
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
quickLookTarget.value = n;
|
|
2246
|
+
quickLookOpen.value = true;
|
|
2247
|
+
void markRecent(n);
|
|
2248
|
+
})();
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
/* /wiring:e2 */
|
|
2172
2252
|
quickLookTarget.value = n;
|
|
2173
2253
|
quickLookOpen.value = true;
|
|
2174
2254
|
void markRecent(n);
|
|
@@ -2196,6 +2276,21 @@ watch(
|
|
|
2196
2276
|
(nodes) => {
|
|
2197
2277
|
if (!quickLookOpen.value) return;
|
|
2198
2278
|
const n = nodes.length === 1 && nodes[0].type === 'file' ? nodes[0] : null;
|
|
2279
|
+
/* wiring:e2 — ok tuşlarıyla gezerken de hedef atanmadan ÖNCE çöz,
|
|
2280
|
+
yoksa viewer bir anlığına ham ciphertext URL'i alır. */
|
|
2281
|
+
if (n && n.path !== quickLookTarget.value?.path && e2eUnlocked.value) {
|
|
2282
|
+
void (async () => {
|
|
2283
|
+
try {
|
|
2284
|
+
await e2eFetchDecrypted(n);
|
|
2285
|
+
} catch {
|
|
2286
|
+
/* çözülemedi — hedefi yine de değiştir, viewer hata gösterir */
|
|
2287
|
+
}
|
|
2288
|
+
quickLookTarget.value = n;
|
|
2289
|
+
void markRecent(n);
|
|
2290
|
+
})();
|
|
2291
|
+
return;
|
|
2292
|
+
}
|
|
2293
|
+
/* /wiring:e2 */
|
|
2199
2294
|
if (n && n.path !== quickLookTarget.value?.path) {
|
|
2200
2295
|
quickLookTarget.value = n;
|
|
2201
2296
|
void markRecent(n);
|
|
@@ -2532,6 +2627,225 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
|
|
|
2532
2627
|
void transferItems(p.sources, p.targetWire, p.originWire);
|
|
2533
2628
|
}
|
|
2534
2629
|
/* === /wiring:d1 === */
|
|
2630
|
+
|
|
2631
|
+
/* === wiring:e2 — uçtan uca şifreli klasörler ===
|
|
2632
|
+
*
|
|
2633
|
+
* Kripto şeması + tehdit modeli: docs/E2E-ENCRYPTION.md ve lib/e2ecrypto.ts.
|
|
2634
|
+
* Burada yalnız orkestrasyon var: backend listing yanıtındaki `e2e_root`
|
|
2635
|
+
* kilit ekranını sürer; parola marker'a karşı TARAYICIDA doğrulanır (sunucuya
|
|
2636
|
+
* hiçbir şey gitmez); türetilen klasör anahtarı (KEK) YALNIZ bellekte yaşar
|
|
2637
|
+
* (`e2eRing`) — localStorage/sessionStorage'a asla yazılmaz. Upload şeffaf
|
|
2638
|
+
* şifrelenir, önizleme/indirme şeffaf çözülür (blob URL mevcut viewer'lara).
|
|
2639
|
+
*/
|
|
2640
|
+
const e2eRing = createKeyRing();
|
|
2641
|
+
// Map'ler reaktif değil — sürüm sayacı computed'ları tetikler.
|
|
2642
|
+
const e2eRingVer = ref(0);
|
|
2643
|
+
// İçinde bulunulan şifreli kökün wire yolu ('' = şifreli bağlam yok).
|
|
2644
|
+
const e2eRoot = ref('');
|
|
2645
|
+
// path → çözülmüş blob objectURL (önizleme). Kilitleme/unmount'ta revoke.
|
|
2646
|
+
const e2eUrls = new Map<string, string>();
|
|
2647
|
+
|
|
2648
|
+
const e2eActive = computed(() => !!e2eRoot.value && !trashMode.value);
|
|
2649
|
+
const e2eUnlocked = computed(() => {
|
|
2650
|
+
void e2eRingVer.value;
|
|
2651
|
+
return e2eActive.value && e2eRing.has(e2eRoot.value);
|
|
2652
|
+
});
|
|
2653
|
+
const e2eLocked = computed(() => {
|
|
2654
|
+
void e2eRingVer.value;
|
|
2655
|
+
return e2eActive.value && !e2eRing.has(e2eRoot.value);
|
|
2656
|
+
});
|
|
2657
|
+
|
|
2658
|
+
// Kilit ekranı formu.
|
|
2659
|
+
const e2ePw = ref('');
|
|
2660
|
+
const e2eUnlockBusy = ref(false);
|
|
2661
|
+
const e2eUnlockErr = ref('');
|
|
2662
|
+
// Şifreli klasör oluşturma modalı.
|
|
2663
|
+
const showEncFolder = ref(false);
|
|
2664
|
+
const e2eCreateBusy = ref(false);
|
|
2665
|
+
|
|
2666
|
+
function e2eKek(): CryptoKey | null {
|
|
2667
|
+
return e2eRing.get(e2eRoot.value) ?? null;
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
function e2eRevokeAll() {
|
|
2671
|
+
for (const url of e2eUrls.values()) URL.revokeObjectURL(url);
|
|
2672
|
+
e2eUrls.clear();
|
|
2673
|
+
}
|
|
2674
|
+
onBeforeUnmount(e2eRevokeAll);
|
|
2675
|
+
|
|
2676
|
+
/** Kilidi aç: marker'ı kökten çek, parolayı YERELDE doğrula, KEK'i belleğe koy. */
|
|
2677
|
+
async function e2eUnlock() {
|
|
2678
|
+
if (!e2ePw.value || e2eUnlockBusy.value || !e2eRoot.value) return;
|
|
2679
|
+
e2eUnlockBusy.value = true;
|
|
2680
|
+
e2eUnlockErr.value = '';
|
|
2681
|
+
try {
|
|
2682
|
+
let markerText = '';
|
|
2683
|
+
try {
|
|
2684
|
+
const { blob, url } = await api.fetchBlob(wireJoin(e2eRoot.value, E2E_MARKER_NAME));
|
|
2685
|
+
URL.revokeObjectURL(url);
|
|
2686
|
+
markerText = await blob.text();
|
|
2687
|
+
} catch {
|
|
2688
|
+
e2eUnlockErr.value = t('e2e.unlock.marker_missing');
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
const marker = parseMarker(markerText);
|
|
2692
|
+
if (!marker) {
|
|
2693
|
+
e2eUnlockErr.value = t('e2e.unlock.marker_missing');
|
|
2694
|
+
return;
|
|
2695
|
+
}
|
|
2696
|
+
const kek = await verifyPassword(marker, e2ePw.value);
|
|
2697
|
+
if (!kek) {
|
|
2698
|
+
e2eUnlockErr.value = t('e2e.unlock.wrong');
|
|
2699
|
+
return;
|
|
2700
|
+
}
|
|
2701
|
+
e2eRing.set(e2eRoot.value, kek);
|
|
2702
|
+
e2eRingVer.value++;
|
|
2703
|
+
e2ePw.value = '';
|
|
2704
|
+
} finally {
|
|
2705
|
+
e2eUnlockBusy.value = false;
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
|
|
2709
|
+
/** "Kilitle": bellekteki anahtarı ve çözülmüş blob'ları at. */
|
|
2710
|
+
function e2eLock() {
|
|
2711
|
+
if (!e2eRoot.value) return;
|
|
2712
|
+
e2eRing.lock(e2eRoot.value);
|
|
2713
|
+
e2eRingVer.value++;
|
|
2714
|
+
e2eRevokeAll();
|
|
2715
|
+
flashToast(t('e2e.locked_toast'));
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// Uzantı → önizleme MIME'ı: çözülmüş blob'un <img>/<video>/<object>
|
|
2719
|
+
// etiketlerinde doğru render'ı için (sunucu şifreli dosyayı octet-stream
|
|
2720
|
+
// bilir, oradan gelen tip işe yaramaz).
|
|
2721
|
+
const E2E_MIME: Record<string, string> = {
|
|
2722
|
+
txt: 'text/plain', md: 'text/markdown', log: 'text/plain', csv: 'text/csv',
|
|
2723
|
+
json: 'application/json', xml: 'application/xml', html: 'text/html',
|
|
2724
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif',
|
|
2725
|
+
webp: 'image/webp', bmp: 'image/bmp', avif: 'image/avif', svg: 'image/svg+xml',
|
|
2726
|
+
pdf: 'application/pdf',
|
|
2727
|
+
mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', m4v: 'video/mp4',
|
|
2728
|
+
mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', flac: 'audio/flac',
|
|
2729
|
+
m4a: 'audio/mp4', aac: 'audio/aac', opus: 'audio/opus',
|
|
2730
|
+
};
|
|
2731
|
+
function e2eMimeFor(n: FileNode): string {
|
|
2732
|
+
const ext = (n.extension || '').toLowerCase();
|
|
2733
|
+
return E2E_MIME[ext] || 'application/octet-stream';
|
|
2734
|
+
}
|
|
2735
|
+
|
|
2736
|
+
/**
|
|
2737
|
+
* Dosyayı çek + çöz + objectURL'ini cache'le. Magic'siz (ör. DAV'la düz
|
|
2738
|
+
* yazılmış) dosyada null döner — çağıran normal ham akışa düşer. Yanlış
|
|
2739
|
+
* anahtar/bozuk veri E2eDecryptError fırlatır (çağıran toast'lar).
|
|
2740
|
+
*/
|
|
2741
|
+
async function e2eFetchDecrypted(n: FileNode): Promise<string | null> {
|
|
2742
|
+
const cached = e2eUrls.get(n.path);
|
|
2743
|
+
if (cached) return cached;
|
|
2744
|
+
const kek = e2eKek();
|
|
2745
|
+
if (!kek) return null;
|
|
2746
|
+
const buf = await api.fetchArrayBuffer(n.path);
|
|
2747
|
+
if (!hasMagic(buf)) return null;
|
|
2748
|
+
const plain = await decryptFile(kek, buf);
|
|
2749
|
+
const url = URL.createObjectURL(new Blob([plain], { type: e2eMimeFor(n) }));
|
|
2750
|
+
e2eUrls.set(n.path, url);
|
|
2751
|
+
return url;
|
|
2752
|
+
}
|
|
2753
|
+
|
|
2754
|
+
/** PreviewModal/QuickLook'a giden URL sağlayıcı: çözülmüş blob > ham URL. */
|
|
2755
|
+
function e2ePreviewSrc(p: string): string {
|
|
2756
|
+
return e2eUrls.get(p) ?? api.previewUrl(p);
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
/** Çöz + salt-okunur in-page önizleme (openNode/previewNode buraya iner). */
|
|
2760
|
+
async function e2eOpenPreview(n: FileNode) {
|
|
2761
|
+
try {
|
|
2762
|
+
await e2eFetchDecrypted(n);
|
|
2763
|
+
} catch {
|
|
2764
|
+
flashToast(t('e2e.decrypt_failed'));
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2767
|
+
previewMode.value = 'view';
|
|
2768
|
+
previewTarget.value = n;
|
|
2769
|
+
showPreview.value = true;
|
|
2770
|
+
emit('file-opened', { path: n.path, basename: n.basename });
|
|
2771
|
+
void markRecent(n);
|
|
2772
|
+
}
|
|
2773
|
+
|
|
2774
|
+
/** Çöz + orijinal adla indir. Magic'siz dosya olduğu gibi iner. */
|
|
2775
|
+
async function e2eDownload(n: FileNode) {
|
|
2776
|
+
try {
|
|
2777
|
+
const buf = await api.fetchArrayBuffer(n.path);
|
|
2778
|
+
const kek = e2eKek();
|
|
2779
|
+
let out = buf;
|
|
2780
|
+
if (hasMagic(buf)) {
|
|
2781
|
+
if (!kek) throw new Error('locked');
|
|
2782
|
+
out = await decryptFile(kek, buf);
|
|
2783
|
+
}
|
|
2784
|
+
const url = URL.createObjectURL(new Blob([out], { type: e2eMimeFor(n) }));
|
|
2785
|
+
const a = document.createElement('a');
|
|
2786
|
+
a.href = url;
|
|
2787
|
+
a.download = n.basename;
|
|
2788
|
+
document.body.appendChild(a);
|
|
2789
|
+
a.click();
|
|
2790
|
+
a.remove();
|
|
2791
|
+
setTimeout(() => URL.revokeObjectURL(url), 30_000);
|
|
2792
|
+
} catch {
|
|
2793
|
+
flashToast(t('e2e.download.failed'));
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
/** Upload listesi → şifreli File listesi (200MB üstü + marker adı atlanır). */
|
|
2798
|
+
async function e2eEncryptUploads(list: File[]): Promise<File[]> {
|
|
2799
|
+
const kek = e2eKek();
|
|
2800
|
+
if (!kek) return [];
|
|
2801
|
+
const out: File[] = [];
|
|
2802
|
+
for (const f of list) {
|
|
2803
|
+
if (f.name === E2E_MARKER_NAME) continue;
|
|
2804
|
+
if (f.size > E2E_MAX_FILE_BYTES) {
|
|
2805
|
+
flashToast(t('e2e.upload.too_big'));
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
try {
|
|
2809
|
+
const ct = await encryptFile(kek, await f.arrayBuffer());
|
|
2810
|
+
out.push(new File([ct], f.name, { type: 'application/octet-stream' }));
|
|
2811
|
+
} catch (err) {
|
|
2812
|
+
emit('error', { message: (err as Error).message, context: { op: 'e2e-encrypt', file: f.name } });
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
return out;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
/** EncryptedFolderModal submit'i: klasörü aç + marker'ı yükle + kilidi açık bırak. */
|
|
2819
|
+
async function submitEncryptedFolder(payload: { name: string; password: string }) {
|
|
2820
|
+
if (e2eActive.value) {
|
|
2821
|
+
// İç içe şifreli klasör kök tespitini bulanıklaştırır — MVP'de yok.
|
|
2822
|
+
flashToast(t('e2e.create.nested'));
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
e2eCreateBusy.value = true;
|
|
2826
|
+
try {
|
|
2827
|
+
const dirWire = qualify(currentPath.value);
|
|
2828
|
+
await api.newFolder(dirWire, payload.name);
|
|
2829
|
+
const { marker, kek } = await createMarker(payload.password);
|
|
2830
|
+
const markerFile = new File([JSON.stringify(marker)], E2E_MARKER_NAME, {
|
|
2831
|
+
type: 'application/json',
|
|
2832
|
+
});
|
|
2833
|
+
const newDirWire = wireJoin(dirWire, payload.name);
|
|
2834
|
+
await api.uploadMultipart(newDirWire, [markerFile]);
|
|
2835
|
+
// Oluşturan oturumda kilit açık başlar (parolayı az önce kendisi girdi).
|
|
2836
|
+
e2eRing.set(newDirWire, kek);
|
|
2837
|
+
e2eRingVer.value++;
|
|
2838
|
+
showEncFolder.value = false;
|
|
2839
|
+
flashToast(t('e2e.create.done'));
|
|
2840
|
+
await load();
|
|
2841
|
+
} catch (err) {
|
|
2842
|
+
emit('error', { message: (err as Error).message, context: { op: 'e2e-create' } });
|
|
2843
|
+
flashToast(t('e2e.create.failed'));
|
|
2844
|
+
} finally {
|
|
2845
|
+
e2eCreateBusy.value = false;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
/* === /wiring:e2 === */
|
|
2535
2849
|
</script>
|
|
2536
2850
|
|
|
2537
2851
|
<template>
|
|
@@ -2627,6 +2941,17 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
|
|
|
2627
2941
|
</span>
|
|
2628
2942
|
</div>
|
|
2629
2943
|
|
|
2944
|
+
<!-- wiring:e2 — kilit açık şeridi: şifreli klasörde anahtar bellekteyken
|
|
2945
|
+
görünür; "Kilitle" anahtarı ve çözülmüş blob'ları atar. -->
|
|
2946
|
+
<div v-if="e2eUnlocked" class="fe-e2e-strip" role="status">
|
|
2947
|
+
<span class="fe-e2e-strip__icon" aria-hidden="true">🔒</span>
|
|
2948
|
+
<span class="fe-e2e-strip__label">{{ t('e2e.strip.label') }}</span>
|
|
2949
|
+
<button type="button" class="fe-btn fe-e2e-strip__btn" @click="e2eLock">
|
|
2950
|
+
{{ t('e2e.strip.lock') }}
|
|
2951
|
+
</button>
|
|
2952
|
+
</div>
|
|
2953
|
+
<!-- /wiring:e2 -->
|
|
2954
|
+
|
|
2630
2955
|
<!-- koru:k1 — fe__main lays the listing body and the inspector panel out
|
|
2631
2956
|
as flex siblings (row). Without the inspector open it is visually
|
|
2632
2957
|
identical to the previous direct-child fe__body. -->
|
|
@@ -2720,6 +3045,49 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
|
|
|
2720
3045
|
</details>
|
|
2721
3046
|
<!-- /wiring:c4 -->
|
|
2722
3047
|
</div>
|
|
3048
|
+
<!-- wiring:e2 — şifreli klasör kilit ekranı: parola doğru girilene dek
|
|
3049
|
+
listeleme render edilmez. Parola tarayıcıda marker'a karşı
|
|
3050
|
+
doğrulanır; sunucuya gitmez. -->
|
|
3051
|
+
<div v-else-if="e2eLocked" class="fe-state fe-e2e-lock">
|
|
3052
|
+
<svg
|
|
3053
|
+
class="fe-state__art"
|
|
3054
|
+
viewBox="0 0 120 100"
|
|
3055
|
+
width="110"
|
|
3056
|
+
height="92"
|
|
3057
|
+
fill="none"
|
|
3058
|
+
stroke="currentColor"
|
|
3059
|
+
stroke-width="2"
|
|
3060
|
+
stroke-linecap="round"
|
|
3061
|
+
stroke-linejoin="round"
|
|
3062
|
+
aria-hidden="true"
|
|
3063
|
+
>
|
|
3064
|
+
<rect x="38" y="44" width="44" height="34" rx="6" />
|
|
3065
|
+
<path d="M46 44v-8a14 14 0 0 1 28 0v8" />
|
|
3066
|
+
<circle cx="60" cy="59" r="3" fill="currentColor" stroke="none" />
|
|
3067
|
+
<path d="M60 62v7" />
|
|
3068
|
+
</svg>
|
|
3069
|
+
<p class="fe-state__title">{{ t('e2e.locked.title') }}</p>
|
|
3070
|
+
<p class="fe-state__hint">{{ t('e2e.locked.hint') }}</p>
|
|
3071
|
+
<form class="fe-e2e-lock__form" @submit.prevent="e2eUnlock">
|
|
3072
|
+
<input
|
|
3073
|
+
v-model="e2ePw"
|
|
3074
|
+
type="password"
|
|
3075
|
+
class="fe-input fe-e2e-lock__input"
|
|
3076
|
+
:placeholder="t('e2e.locked.pw_placeholder')"
|
|
3077
|
+
autocomplete="current-password"
|
|
3078
|
+
:disabled="e2eUnlockBusy"
|
|
3079
|
+
/>
|
|
3080
|
+
<button
|
|
3081
|
+
type="submit"
|
|
3082
|
+
class="fe-btn fe-btn--primary"
|
|
3083
|
+
:disabled="e2eUnlockBusy || !e2ePw"
|
|
3084
|
+
>
|
|
3085
|
+
{{ e2eUnlockBusy ? t('e2e.locked.busy') : t('e2e.locked.unlock') }}
|
|
3086
|
+
</button>
|
|
3087
|
+
</form>
|
|
3088
|
+
<p v-if="e2eUnlockErr" class="fe-form__error" role="alert">{{ e2eUnlockErr }}</p>
|
|
3089
|
+
</div>
|
|
3090
|
+
<!-- /wiring:e2 -->
|
|
2723
3091
|
<!-- Search with zero hits — its own message, not "folder is empty". -->
|
|
2724
3092
|
<div v-else-if="!loading && files.length === 0 && searchQuery" class="fe-state">
|
|
2725
3093
|
<svg
|
|
@@ -2960,9 +3328,20 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
|
|
|
2960
3328
|
<NewFolderModal
|
|
2961
3329
|
:open="showNewFolder"
|
|
2962
3330
|
:locale="locale"
|
|
3331
|
+
:encrypted-option="!e2eActive /* wiring:e2 — iç içe şifreli klasör yok */"
|
|
2963
3332
|
@close="showNewFolder = false"
|
|
2964
3333
|
@submit="submitNewFolder"
|
|
3334
|
+
@encrypted="showNewFolder = false; showEncFolder = true /* wiring:e2 */"
|
|
3335
|
+
/>
|
|
3336
|
+
<!-- wiring:e2 — şifreli klasör oluşturma modalı -->
|
|
3337
|
+
<EncryptedFolderModal
|
|
3338
|
+
:open="showEncFolder"
|
|
3339
|
+
:locale="locale"
|
|
3340
|
+
:busy="e2eCreateBusy"
|
|
3341
|
+
@close="showEncFolder = false"
|
|
3342
|
+
@submit="submitEncryptedFolder"
|
|
2965
3343
|
/>
|
|
3344
|
+
<!-- /wiring:e2 -->
|
|
2966
3345
|
<RenameModal
|
|
2967
3346
|
:open="showRename"
|
|
2968
3347
|
:locale="locale"
|
|
@@ -2990,11 +3369,12 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
|
|
|
2990
3369
|
:locale="locale"
|
|
2991
3370
|
:file="previewTarget"
|
|
2992
3371
|
:theme="config.theme || 'auto'"
|
|
2993
|
-
:preview-url="(p) =>
|
|
2994
|
-
:download-url="(p) => api.downloadUrl(p)"
|
|
2995
|
-
:only-office-base="effectiveOnlyOfficeBase"
|
|
3372
|
+
:preview-url="(p) => e2ePreviewSrc(p) /* wiring:e2 — çözülmüş blob > ham URL */"
|
|
3373
|
+
:download-url="(p) => (e2eUnlocked ? e2ePreviewSrc(p) : api.downloadUrl(p)) /* wiring:e2 */"
|
|
3374
|
+
:only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 — OO ciphertext açamaz */"
|
|
2996
3375
|
:only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
|
|
2997
|
-
:
|
|
3376
|
+
:new-tab-enabled="!e2eActive /* wiring:e2 — standalone rota ham baytı çeker */"
|
|
3377
|
+
:save-text-endpoint="e2eActive ? null : api.endpoints.saveText || null /* wiring:e2 — düz metin kaydı sızıntı olur */"
|
|
2998
3378
|
:open-mode="previewMode"
|
|
2999
3379
|
:auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
|
|
3000
3380
|
:auth-credentials="api.credentialsMode()"
|
|
@@ -3165,9 +3545,9 @@ function onPaneTransfer(p: { sources: string[]; targetWire: string; originWire?:
|
|
|
3165
3545
|
:locale="locale"
|
|
3166
3546
|
:file="quickLookTarget"
|
|
3167
3547
|
:theme="config.theme || 'auto'"
|
|
3168
|
-
:preview-url="(p: string) =>
|
|
3169
|
-
:download-url="(p: string) => api.downloadUrl(p)"
|
|
3170
|
-
:only-office-base="effectiveOnlyOfficeBase"
|
|
3548
|
+
:preview-url="(p: string) => e2ePreviewSrc(p) /* wiring:e2 */"
|
|
3549
|
+
:download-url="(p: string) => (e2eUnlocked ? e2ePreviewSrc(p) : api.downloadUrl(p)) /* wiring:e2 */"
|
|
3550
|
+
:only-office-base="e2eActive ? null : effectiveOnlyOfficeBase /* wiring:e2 */"
|
|
3171
3551
|
:only-office-config-endpoint="effectiveOnlyOfficeConfigEndpoint"
|
|
3172
3552
|
:auth-headers="() => buildAuthHeaders({ 'Content-Type': 'application/json' })"
|
|
3173
3553
|
:auth-credentials="api.credentialsMode()"
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* EncryptedFolderModal — create an E2E-encrypted folder (wiring:e2).
|
|
4
|
+
*
|
|
5
|
+
* Collects folder name + password ×2 and an explicit "I understand there
|
|
6
|
+
* is NO recovery" acknowledgement. The parent (FileExplorer) performs the
|
|
7
|
+
* actual newfolder + marker-upload dance; this modal never touches the
|
|
8
|
+
* network and never stores the password anywhere.
|
|
9
|
+
*
|
|
10
|
+
* Crypto scheme + threat model: docs/E2E-ENCRYPTION.md.
|
|
11
|
+
*/
|
|
12
|
+
import { ref, watch } from 'vue';
|
|
13
|
+
import type { LocaleCode } from '../types/ExplorerConfig';
|
|
14
|
+
import { useLocale } from '../composables/useLocale';
|
|
15
|
+
import { E2E_MIN_PASSWORD_LEN } from '../lib/e2ecrypto';
|
|
16
|
+
import Modal from '../modals/Modal.vue';
|
|
17
|
+
|
|
18
|
+
const props = defineProps<{
|
|
19
|
+
open: boolean;
|
|
20
|
+
locale: LocaleCode;
|
|
21
|
+
/** True while the parent is creating the folder + uploading the marker. */
|
|
22
|
+
busy?: boolean;
|
|
23
|
+
}>();
|
|
24
|
+
|
|
25
|
+
const emit = defineEmits<{
|
|
26
|
+
(e: 'close'): void;
|
|
27
|
+
(e: 'submit', payload: { name: string; password: string }): void;
|
|
28
|
+
}>();
|
|
29
|
+
|
|
30
|
+
const { t } = useLocale(() => props.locale);
|
|
31
|
+
const name = ref('');
|
|
32
|
+
const password = ref('');
|
|
33
|
+
const password2 = ref('');
|
|
34
|
+
const ack = ref(false);
|
|
35
|
+
const err = ref<string | null>(null);
|
|
36
|
+
|
|
37
|
+
watch(
|
|
38
|
+
() => props.open,
|
|
39
|
+
(v) => {
|
|
40
|
+
if (v) {
|
|
41
|
+
name.value = '';
|
|
42
|
+
password.value = '';
|
|
43
|
+
password2.value = '';
|
|
44
|
+
ack.value = false;
|
|
45
|
+
err.value = null;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
function submit() {
|
|
51
|
+
if (props.busy) return;
|
|
52
|
+
const clean = name.value.trim();
|
|
53
|
+
if (!clean) {
|
|
54
|
+
err.value = t('modal.newfolder.placeholder');
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (/[\\/]/.test(clean) || clean === '.' || clean === '..' || clean.startsWith('.filex')) {
|
|
58
|
+
err.value = t('e2e.create.bad_name');
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (password.value.length < E2E_MIN_PASSWORD_LEN) {
|
|
62
|
+
err.value = t('e2e.create.pw_short');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (password.value !== password2.value) {
|
|
66
|
+
err.value = t('e2e.create.pw_mismatch');
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (!ack.value) {
|
|
70
|
+
err.value = t('e2e.create.ack_required');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
err.value = null;
|
|
74
|
+
emit('submit', { name: clean, password: password.value });
|
|
75
|
+
}
|
|
76
|
+
</script>
|
|
77
|
+
|
|
78
|
+
<template>
|
|
79
|
+
<Modal :open="open" :title="t('e2e.create.title')" size="sm" @close="emit('close')">
|
|
80
|
+
<form class="fe-e2e-form" @submit.prevent="submit">
|
|
81
|
+
<input
|
|
82
|
+
v-model="name"
|
|
83
|
+
type="text"
|
|
84
|
+
class="fe-input"
|
|
85
|
+
:placeholder="t('modal.newfolder.placeholder')"
|
|
86
|
+
autocomplete="off"
|
|
87
|
+
:disabled="busy"
|
|
88
|
+
/>
|
|
89
|
+
<input
|
|
90
|
+
v-model="password"
|
|
91
|
+
type="password"
|
|
92
|
+
class="fe-input"
|
|
93
|
+
:placeholder="t('e2e.create.pw_placeholder')"
|
|
94
|
+
autocomplete="new-password"
|
|
95
|
+
:disabled="busy"
|
|
96
|
+
/>
|
|
97
|
+
<input
|
|
98
|
+
v-model="password2"
|
|
99
|
+
type="password"
|
|
100
|
+
class="fe-input"
|
|
101
|
+
:placeholder="t('e2e.create.pw2_placeholder')"
|
|
102
|
+
autocomplete="new-password"
|
|
103
|
+
:disabled="busy"
|
|
104
|
+
@keydown.enter.prevent="submit"
|
|
105
|
+
/>
|
|
106
|
+
<div class="fe-e2e-warn" role="alert">
|
|
107
|
+
<strong>{{ t('e2e.create.warn_title') }}</strong>
|
|
108
|
+
<p>{{ t('e2e.create.warn_body') }}</p>
|
|
109
|
+
</div>
|
|
110
|
+
<label class="fe-e2e-ack">
|
|
111
|
+
<input v-model="ack" type="checkbox" :disabled="busy" />
|
|
112
|
+
<span>{{ t('e2e.create.ack') }}</span>
|
|
113
|
+
</label>
|
|
114
|
+
<p v-if="err" class="fe-form__error">{{ err }}</p>
|
|
115
|
+
</form>
|
|
116
|
+
<template #actions>
|
|
117
|
+
<button type="button" class="fe-btn" :disabled="busy" @click="emit('close')">
|
|
118
|
+
{{ t('modal.newfolder.cancel') }}
|
|
119
|
+
</button>
|
|
120
|
+
<button type="button" class="fe-btn fe-btn--primary" :disabled="busy" @click="submit">
|
|
121
|
+
{{ busy ? t('e2e.create.busy') : t('e2e.create.create') }}
|
|
122
|
+
</button>
|
|
123
|
+
</template>
|
|
124
|
+
</Modal>
|
|
125
|
+
</template>
|
|
@@ -127,6 +127,7 @@ function parentDir(path: string): string {
|
|
|
127
127
|
function specialEmojiFor(n: FileNode): string | null {
|
|
128
128
|
if (n.basename === '.trash') return '🗑';
|
|
129
129
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
130
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
|
|
130
131
|
return null;
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -126,6 +126,7 @@ function parentDir(path: string): string {
|
|
|
126
126
|
function specialEmojiFor(n: FileNode): string | null {
|
|
127
127
|
if (n.basename === '.trash') return '🗑';
|
|
128
128
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
129
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
|
|
129
130
|
return null;
|
|
130
131
|
}
|
|
131
132
|
|
|
@@ -139,6 +139,7 @@ function cancelPress() {
|
|
|
139
139
|
function specialEmojiFor(n: FileNode): string | null {
|
|
140
140
|
if (n.basename === '.trash') return '🗑';
|
|
141
141
|
if (n.mime_type === 'inode/storage') return '💾';
|
|
142
|
+
if (n.type === 'dir' && n.e2e === true) return '🔒'; /* wiring:e2 — şifreli klasör rozeti */
|
|
142
143
|
return null;
|
|
143
144
|
}
|
|
144
145
|
|