1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

wip files demo

This commit is contained in:
William Martin
2024-12-13 08:51:10 -05:00
parent 84dfa3ab05
commit e854197bc5
12 changed files with 319 additions and 1 deletions

View File

@@ -4903,5 +4903,8 @@
},
"extraWide": {
"message": "Extra wide"
},
"files": {
"message": "Files"
}
}

View File

@@ -149,10 +149,21 @@ export class ProductSwitcherService {
external: true,
},
isActive:
!this.router.url.includes("/files") &&
!this.router.url.includes("/sm/") &&
!this.router.url.includes("/organizations/") &&
!this.router.url.includes("/providers/"),
},
files: {
name: "File Manager",
icon: "bwi-file",
appRoute: "/files",
marketingRoute: {
route: "https://bitwarden.com/products/personal/",
external: true,
},
isActive: this.router.url.includes("/files"),
},
sm: {
name: "Secrets Manager",
icon: "bwi-cli",
@@ -196,7 +207,7 @@ export class ProductSwitcherService {
},
} satisfies Record<string, ProductSwitcherItem>;
const bento: ProductSwitcherItem[] = [products.pm];
const bento: ProductSwitcherItem[] = [products.pm, products.files];
const other: ProductSwitcherItem[] = [];
if (smOrg) {

View File

@@ -87,6 +87,8 @@ import { ReportsModule } from "./tools/reports";
import { AccessComponent } from "./tools/send/access.component";
import { SendAccessExplainerComponent } from "./tools/send/send-access-explainer.component";
import { SendComponent } from "./tools/send/send.component";
import { FilesLayoutComponent } from "./vault/files/files-layout.component";
import { FilesComponent } from "./vault/files/files.component";
import { VaultModule } from "./vault/individual-vault/vault.module";
const routes: Routes = [
@@ -663,6 +665,17 @@ const routes: Routes = [
},
],
},
{
path: "files",
component: FilesLayoutComponent,
children: [
{
path: "",
component: FilesComponent,
data: { titleId: "files" } satisfies RouteDataProperties,
},
],
},
{
path: "",
component: UserLayoutComponent,

View File

@@ -0,0 +1,21 @@
<bit-dialog [title]="data.file.attachment.fileName" size="large" background="alt">
<ng-container bitDialogContent>
<ng-container *ngIf="fileType === 'image'">
<img *ngIf="srcUrl" [src]="srcUrl" class="tw-w-full tw-h-full" />
<i *ngIf="!srcUrl" class="bwi bwi-spinner bwi-spin"></i>
</ng-container>
<!-- FIXME: doesn't work -->
<ng-container *ngIf="fileType === 'text' && blob">
<pre>
{{ blob.text() | async }}
</pre
>
</ng-container>
</ng-container>
<ng-container bitDialogFooter>
<button type="button" bitIconButton="bwi-download"></button>
<button type="button" bitIconButton="bwi-star"></button>
<button class="tw-ml-auto" type="button" bitIconButton="bwi-trash" buttonType="danger"></button>
</ng-container>
</bit-dialog>

View File

@@ -0,0 +1,62 @@
import { DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, OnInit, inject } from "@angular/core";
import {
AsyncActionsModule,
ButtonModule,
DialogModule,
DialogService,
} from "@bitwarden/components";
import { SharedModule } from "../../shared";
import { FileService } from "./file.service";
import { FileView } from "./files.component";
export type FilePreviewDialogData = {
file: FileView;
};
@Component({
standalone: true,
templateUrl: "file-preview-dialog.component.html",
imports: [SharedModule, DialogModule, ButtonModule, AsyncActionsModule],
})
export class FilePreviewDialogComponent implements OnInit {
protected data = inject(DIALOG_DATA) as FilePreviewDialogData;
private fileService = inject(FileService);
protected srcUrl?: string;
protected blob?: Blob;
protected get fileType(): string {
return this.fileService.getFileType(this.data.file);
}
protected get icon(): `bwi-${string}` {
switch (this.fileType) {
case "image":
return "bwi-camera";
case "pdf":
return "bwi-pdf";
case "text":
return "bwi-file-text";
default:
return "bwi-file";
}
}
async ngOnInit() {
this.blob = await this.fileService.toBlob(this.data.file);
this.srcUrl = URL.createObjectURL(this.blob);
}
static open(dialogService: DialogService, file: FileView) {
dialogService.open(FilePreviewDialogComponent, {
data: {
file,
},
});
}
}

View File

@@ -0,0 +1,94 @@
import { Injectable, inject } from "@angular/core";
import { Observable, from, map, shareReplay } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SubscriptionResponse } from "@bitwarden/common/billing/models/response/subscription.response";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { EncryptService } from "@bitwarden/common/platform/abstractions/encrypt.service";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { KeyService } from "@bitwarden/key-management";
export type FileView = {
cipher: CipherView;
attachment: AttachmentView;
};
@Injectable({ providedIn: "root" })
export class FileService {
private apiService = inject(ApiService);
private cipherService = inject(CipherService);
private encryptService = inject(EncryptService);
private keyService = inject(KeyService);
files$: Observable<FileView[]> = this.cipherService.cipherViews$.pipe(
map((ciphers) =>
Object.values(ciphers)
.filter((cipher) => Array.isArray(cipher.attachments) && cipher.attachments.length > 0)
.map((cipher) => cipher.attachments.map((attachment) => ({ cipher, attachment })))
.flat(),
),
);
sub$: Observable<SubscriptionResponse> = from(this.apiService.getUserSubscription()).pipe(
shareReplay({ bufferSize: 1, refCount: true }),
);
storagePercentage$: Observable<number> = this.sub$.pipe(
map((sub) =>
sub != null && sub.maxStorageGb ? +(100 * (sub.storageGb / sub.maxStorageGb)).toFixed(2) : 0,
),
);
getFileType(file: FileView): "image" | "pdf" | "text" | "unknown" {
switch (file.attachment.fileName.split(".").at(-1)) {
case "gif":
case "jpeg":
case "jpg":
return "image";
case "pdf":
return "pdf";
case "txt":
return "text";
default:
return "unknown";
}
}
async toBlob(file: FileView): Promise<Blob> {
const { cipher, attachment } = file;
let url: string;
try {
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
cipher.id,
attachment.id,
// this.emergencyAccessId,
);
url = attachmentDownloadResponse.url;
} catch (e) {
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
url = attachment.url;
} else if (e instanceof ErrorResponse) {
throw new Error((e as ErrorResponse).getSingleMessage());
} else {
throw e;
}
}
const response = await fetch(new Request(url, { cache: "no-store" }));
if (response.status !== 200) {
throw new Error("blob download error");
}
const encBuf = await EncArrayBuffer.fromResponse(response);
const key =
attachment.key != null
? attachment.key
: await this.keyService.getOrgKey(cipher.organizationId);
const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
return new Blob([decBuf]);
}
}

View File

@@ -0,0 +1,9 @@
<app-layout>
<app-side-nav>
<bit-nav-logo [openIcon]="$any({})" route="." label="File Manager"></bit-nav-logo>
<bit-nav-item icon="bwi-vault" text="Vaults"></bit-nav-item>
<bit-nav-item icon="bwi-send" text="Send"></bit-nav-item>
<bit-nav-item icon="bwi-cog" text="Settings"></bit-nav-item>
</app-side-nav>
<router-outlet></router-outlet>
</app-layout>

View File

@@ -0,0 +1,13 @@
import { Component } from "@angular/core";
import { RouterModule } from "@angular/router";
import { HeaderModule } from "../../layouts/header/header.module";
import { WebLayoutModule } from "../../layouts/web-layout.module";
import { SharedModule } from "../../shared";
@Component({
standalone: true,
templateUrl: "files-layout.component.html",
imports: [SharedModule, HeaderModule, WebLayoutModule, RouterModule],
})
export class FilesLayoutComponent {}

View File

@@ -0,0 +1,41 @@
<app-header title="File vault">
<bit-search placeholder="Search files" class="tw-w-80"></bit-search>
<button type="button" bitButton buttonType="primary">
<i class="bwi bwi-plus-f tw-mr-2" aria-hidden="true"></i>
{{ "Upload" }}
</button>
</app-header>
<bit-section class="tw-block tw-p-4">
<bit-table-scroll [dataSource]="tableDataSource" [rowSize]="47">
<ng-container header>
<th></th>
<!-- FIXME: sort -->
<th bitCell bitSortable="attachment.fileName">Name</th>
<th bitCell bitSortable="attachment.sizeName">Size</th>
</ng-container>
<ng-template bitRowDef let-row>
<td bitCell>
<i class="bwi bwi-file"></i>
</td>
<td bitCell>
<button type="button" bitLink (click)="openFilePreview(row)">
{{ row.attachment.fileName }}
</button>
</td>
<td bitCell>{{ row.attachment.sizeName }}</td>
</ng-template>
</bit-table-scroll>
<div *ngIf="fileService.sub$ | async as sub" class="tw-flex tw-gap-4 tw-min-w-full tw-mt-4">
<span bitTypography="helper" class="tw-min-w-max">{{
sub.storageName + " / " + sub.maxStorageGb + " GB"
}}</span>
<bit-progress
class="tw-w-full"
[barWidth]="fileService.storagePercentage$ | async"
bgColor="success"
size="default"
></bit-progress>
</div>
</bit-section>

View File

@@ -0,0 +1,47 @@
import { Component, OnInit, inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
DialogService,
NoItemsModule,
SearchModule,
TableDataSource,
TableModule,
} from "@bitwarden/components";
import { HeaderModule } from "../../layouts/header/header.module";
import { SharedModule } from "../../shared";
import { FilePreviewDialogComponent } from "./file-preview-dialog.component";
import { FileService } from "./file.service";
export type FileView = {
cipher: CipherView;
attachment: AttachmentView;
};
@Component({
standalone: true,
templateUrl: "files.component.html",
imports: [SharedModule, HeaderModule, TableModule, SearchModule, NoItemsModule],
})
export class FilesComponent implements OnInit {
protected fileService = inject(FileService);
private dialogService = inject(DialogService);
protected tableDataSource = new TableDataSource();
constructor() {
this.fileService.files$.pipe(takeUntilDestroyed()).subscribe((files) => {
this.tableDataSource.data = files;
});
}
async ngOnInit() {}
protected openFilePreview(file: FileView) {
FilePreviewDialogComponent.open(this.dialogService, file);
}
}

View File

@@ -265,6 +265,8 @@ const devServer =
https://www.paypalobjects.com
https://q.stripe.com
https://haveibeenpwned.com
https://localhost:8080
blob:
;child-src
'self'
https://js.stripe.com

View File

@@ -158,6 +158,7 @@ export class AttachmentsComponent implements OnInit {
this.emergencyAccessId,
);
url = attachmentDownloadResponse.url;
alert(url);
} catch (e) {
if (e instanceof ErrorResponse && (e as ErrorResponse).statusCode === 404) {
url = attachment.url;
@@ -239,6 +240,7 @@ export class AttachmentsComponent implements OnInit {
this.reuploadPromises[attachment.id] = Promise.resolve().then(async () => {
// 1. Download
a.downloading = true;
alert(attachment.url);
const response = await fetch(new Request(attachment.url, { cache: "no-store" }));
if (response.status !== 200) {
this.platformUtilsService.showToast("error", null, this.i18nService.t("errorOccurred"));