后端API接口项目,请参考minio-server项目,使用springboot + minio-client框架实现存储服务。
前端使用Angular + ng-zorro实现,项目源码在这里Gitee
模型(model)
模型在minio.model.ts文件中定义
export class Stat {
etag!: string;
size!: number;
lastModified!: Date;
retentionModal!: string;
retentionRetainUtilDate!: Date;
legalHold!: boolean;
deleteMarker!: boolean;
versionId!: string;
contentTypeL!: string;
userMetadata!: UserMetadata;
constructor(data?: Partial<Stat>) {
Object.assign(this, data);
if (data?.lastModified != undefined) {
this.lastModified = new Date(data.lastModified);
}
if (data?.retentionRetainUtilDate != undefined) {
this.retentionRetainUtilDate = new Date(data.retentionRetainUtilDate);
}
if (data?.userMetadata != undefined) {
this.userMetadata = new UserMetadata(data.userMetadata);
}
}
}
export class Item {
etag!: string;
objectName!: string;
lastModified!: Date;
owner!: string;
size!: number;
storageClass!: string;
isLastest!: boolean;
versionId!: string;
isDir!: boolean;
isDeleteMarker!: boolean;
userMetadata!: UserMetadata;
constructor(data?: Partial<Item>) {
Object.assign(this, data);
if (data?.lastModified != undefined) {
this.lastModified = new Date(data.lastModified);
}
if (data?.userMetadata != undefined) {
this.userMetadata = new UserMetadata(data.userMetadata);
}
}
}
export class UserMetadata {
objectid!: string;
bucket!: string;
minioid!: string;
filename!: string;
filesize!: number;
createdon!: Date;
createdby!: string;
contenttype!: string;
constructor(data?: Partial<UserMetadata>) {
Object.assign(this, data);
if (data?.createdon != undefined) {
this.createdon = new Date(data.createdon);
}
}
}
State, Item, UserMetadata是MinIO client 中的对象。
服务(sevice)
服务在minio.service.ts文件中定义
import { Injectable } from '@angular/core';
import { map, Observable, Subject, tap } from 'rxjs';
import { BaseHttpService } from '@core/base/base-http.service';
import { Item, Stat, UserMetadata } from './minio.model';
@Injectable({ providedIn: 'root' })
export class MinioService {
private baseGetUrl = '/oss/v1/getservice';
private baseListUrl = '/oss/v1/listservice';
private basePutUrl = '/oss/v1/putservice';
private baseRemoveUrl = '/oss/v1/removeservice';
private baseStatUrl = '/oss/v1/statservice';
private bucketName = 'megrez';
private upload$ = new Subject<UserMetadata>();
private remove$ = new Subject<UserMetadata>();
constructor(private http: BaseHttpService) {}
public get onUpload(): Observable<UserMetadata> {
return this.upload$.asObservable();
}
public get onRemove(): Observable<UserMetadata> {
return this.remove$.asObservable();
}
upload(formData: FormData): Observable<UserMetadata> {
return this.http.post<UserMetadata>(`${this.basePutUrl}/object/${this.bucketName}`, formData).pipe(
map(e => new UserMetadata(e)),
tap(e => {
this.upload$.next(e);
})
);
}
download(objectId: string): Observable<Blob> {
return this.http.get(`${this.baseGetUrl}/object/${this.bucketName}/${objectId}`, null, {
responseType: 'blob'
});
}
downloads(objectIds: string[]): Observable<Blob> {
const queryParams = { objectIds: objectIds };
return this.http.get(`${this.baseGetUrl}/objects/${this.bucketName}`, queryParams, { responseType: 'blob' });
}
getPresignedObjectUrl(objectId: string, method: string): Observable<string> {
return this.http.get(`${this.baseGetUrl}/presigned-url/${this.bucketName}/${objectId}/${method}`, null, {
responseType: 'text'
});
}
delete(objectId: string): Observable<UserMetadata> {
const url = `${this.baseRemoveUrl}/object/${this.bucketName}/${objectId}`;
return this.http.delete<UserMetadata>(url).pipe(
map(e => new UserMetadata(e)),
tap(e => {
this.remove$.next(new UserMetadata(e));
})
);
}
list(prefix: string, includeUserMetadata?: boolean): Observable<Item[]> {
const queryParams = { prefix: prefix, includeUserMetadata: includeUserMetadata };
return this.http
.get<Item[]>(`${this.baseListUrl}/objects/${this.bucketName}`, queryParams)
.pipe(map(data => data.map(e => new Item(e))));
}
getStat(objectId: string): Observable<Stat> {
return this.http.get<Stat>(`${this.baseStatUrl}/state/${this.bucketName}/${objectId}`).pipe(map(e => new Stat(e)));
}
getStats(objectIds: string[]): Observable<Stat[]> {
const queryParams = { objectIds: objectIds };
return this.http
.get<Stat[]>(`${this.baseStatUrl}/state/${this.bucketName}`, queryParams)
.pipe(map(data => data.map(e => new Stat(e))));
}
getUserMetadata(objectId: string): Observable<UserMetadata> {
return this.http
.get<UserMetadata>(`${this.baseStatUrl}/usermetadata/${this.bucketName}/${objectId}`)
.pipe(map(e => new UserMetadata(e)));
}
getUserMetadatas(objectIds: string[]): Observable<UserMetadata[]> {
const queryParams = { objectIds: objectIds };
return this.http
.get<UserMetadata[]>(`${this.baseStatUrl}/usermetadata/${this.bucketName}`, queryParams)
.pipe(map(data => data.map(e => new UserMetadata(e))));
}
}
功能包括,文件上传,下载,获取用户元数据等。
功能演示
演示功能在minio-action.component.html及minio-action.component.ts中
import { Component } from '@angular/core';
import { forkJoin, Observable } from 'rxjs';
import { FileService } from '@core/base';
import { MinioService, UserMetadata } from '@core/oss/minio';
import { NzMessageService } from 'ng-zorro-antd/message';
import { NzUploadFile } from 'ng-zorro-antd/upload';
@Component({
selector: 'app-minio-action',
templateUrl: './minio-action.component.html'
})
export class MinioActionComponent {
fileList: NzUploadFile[] = [];
userMatadatas: UserMetadata[] = [];
downloadIds: string = '';
presignedUrlId?: string;
deleteId?: string;
isLoading = false;
presignedUrl?: string;
method: string = 'GET';
constructor(private filesSrc: FileService, private minioSrc: MinioService, private messageSrc: NzMessageService) {}
beforeUpload = (file: NzUploadFile): boolean => {
this.fileList = this.fileList.concat(file);
return false;
};
handleUpload(): void {
const obserList: Array<Observable<any>> = [];
this.fileList.forEach((file: any) => {
const formData = new FormData();
formData.append('file', file);
obserList.push(this.minioSrc.upload(formData));
});
this.isLoading = true;
forkJoin(obserList).subscribe({
next: (data: any) => {
this.isLoading = false;
this.fileList = [];
this.userMatadatas = this.userMatadatas.concat(data);
this.messageSrc.success('upload successfully.');
},
error: () => {
this.isLoading = false;
this.messageSrc.error('upload failed.');
}
});
}
download(): void {
const idList = this.downloadIds.split(',').filter(e => e.trim().length > 0);
if (idList.length === 0) {
return;
}
this.isLoading = true;
if (idList.length === 1) {
this.minioSrc.download(idList[0]).subscribe(data => {
this.filesSrc.saveAsFile(data, '');
this.isLoading = false;
});
} else {
this.minioSrc.downloads(idList).subscribe(data => {
this.filesSrc.saveAsFile(data, '');
this.isLoading = false;
});
}
}
delete(): void {
if (!this.deleteId) {
return;
}
this.isLoading = true;
this.minioSrc.delete(this.deleteId).subscribe(() => {
this.messageSrc.success('删除成功');
this.isLoading = false;
});
}
getPresignedUrl(): void {
if (!this.presignedUrlId) {
return;
}
this.isLoading = true;
this.minioSrc.getPresignedObjectUrl(this.presignedUrlId, this.method).subscribe(data => {
this.presignedUrl = data;
this.isLoading = false;
});
}
}
<nz-spin [nzSpinning]="isLoading">
<nz-upload [(nzFileList)]="fileList" [nzBeforeUpload]="beforeUpload">
<button nz-button>
<span nz-icon nzType="upload"></span>
Select File
</button>
</nz-upload>
<button
nz-button
[nzType]="'primary'"
(click)="handleUpload()"
[disabled]="fileList.length === 0"
style="margin-top: 16px"
>
{{ isLoading ? 'Uploading' : 'Start Upload' }}
</button>
<div style="margin-top: 8px">
<textarea rows="10" nz-input [ngModel]="userMatadatas | json" *ngIf="userMatadatas.length > 0"></textarea>
<nz-empty *ngIf="userMatadatas.length === 0"></nz-empty>
</div>
<div style="margin-top: 25px">
<nz-space>
<input
*nzSpaceItem
nz-input
placeholder="输入objectId, 多个使用','相隔"
[(ngModel)]="downloadIds"
style="width: 300px"
/>
<button *nzSpaceItem nz-button nzType="primary" (click)="download()">下载文件</button>
</nz-space>
</div>
<div style="margin-top: 25px">
<nz-space>
<input *nzSpaceItem nz-input placeholder="输入一个objectId" [(ngModel)]="deleteId" style="width: 300px" />
<button *nzSpaceItem nz-button nzType="primary" (click)="delete()"> 删除 </button>
</nz-space>
</div>
<div style="margin-top: 25px">
<nz-space>
<input *nzSpaceItem nz-input placeholder="输入一个objectId" [(ngModel)]="presignedUrlId" style="width: 300px" />
<nz-radio-group *nzSpaceItem [(ngModel)]="method">
<label nz-radio nzValue="GET">GET</label>
<label nz-radio nzValue="POST">POST</label>
<label nz-radio nzValue="PUT">PUT</label>
<label nz-radio nzValue="DELETE">DELETE</label>
</nz-radio-group>
<button *nzSpaceItem nz-button nzType="primary" (click)="getPresignedUrl()"> 获取PresignedUrl </button>
</nz-space>
<textarea style="margin-top: 8px" rows="4" nz-input [ngModel]="presignedUrl" readonly></textarea>
</div>
</nz-spin>
功能截图
文件上传:
其他功能:
获取元数据: