Ability to add and delete files
This commit is contained in:
parent
236caa2616
commit
a95d37546f
9 changed files with 449 additions and 19 deletions
57
package-lock.json
generated
57
package-lock.json
generated
|
@ -17,14 +17,18 @@
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^17.0.39",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-bootstrap": "^0.24.5",
|
"@types/react-router-bootstrap": "^0.24.5",
|
||||||
|
"@types/streamsaver": "^2.0.1",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^2.1.2",
|
"react-bootstrap": "^2.1.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-icons": "^4.3.1",
|
||||||
"react-router-bootstrap": "^0.26.0",
|
"react-router-bootstrap": "^0.26.0",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
|
"streamsaver": "^2.0.6",
|
||||||
"typescript": "^4.5.5",
|
"typescript": "^4.5.5",
|
||||||
|
"web-streams-polyfill": "^3.2.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -4982,6 +4986,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/streamsaver": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/streamsaver/-/streamsaver-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-I49NtT8w6syBI3Zg3ixCyygTHoTVMY0z2TMRcTgccdIsVd2MwlKk7ITLHLsJtgchUHcOd7QEARG9h0ifcA6l2Q=="
|
||||||
|
},
|
||||||
"node_modules/@types/testing-library__jest-dom": {
|
"node_modules/@types/testing-library__jest-dom": {
|
||||||
"version": "5.14.2",
|
"version": "5.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz",
|
||||||
|
@ -15584,6 +15593,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
|
||||||
"integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA=="
|
"integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-icons": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
@ -16637,6 +16654,17 @@
|
||||||
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
|
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/streamsaver": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsaver/-/streamsaver-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-LK4e7TfCV8HzuM0PKXuVUfKyCB1FtT9L0EGxsFk5Up8njj0bXK8pJM9+Wq2Nya7/jslmCQwRK39LFm55h7NBTw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/string_decoder": {
|
"node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
@ -17777,6 +17805,14 @@
|
||||||
"minimalistic-assert": "^1.0.0"
|
"minimalistic-assert": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/web-vitals": {
|
"node_modules/web-vitals": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
||||||
|
@ -22365,6 +22401,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz",
|
||||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw=="
|
||||||
},
|
},
|
||||||
|
"@types/streamsaver": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/streamsaver/-/streamsaver-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-I49NtT8w6syBI3Zg3ixCyygTHoTVMY0z2TMRcTgccdIsVd2MwlKk7ITLHLsJtgchUHcOd7QEARG9h0ifcA6l2Q=="
|
||||||
|
},
|
||||||
"@types/testing-library__jest-dom": {
|
"@types/testing-library__jest-dom": {
|
||||||
"version": "5.14.2",
|
"version": "5.14.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz",
|
||||||
|
@ -30053,6 +30094,12 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz",
|
||||||
"integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA=="
|
"integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA=="
|
||||||
},
|
},
|
||||||
|
"react-icons": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-cB10MXLTs3gVuXimblAdI71jrJx8njrJZmNMEMC+sQu5B/BIOmlsAjskdqpn81y8UBVEGuHODd7/ci5DvoSzTQ==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
@ -30853,6 +30900,11 @@
|
||||||
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
|
"integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"streamsaver": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamsaver/-/streamsaver-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-LK4e7TfCV8HzuM0PKXuVUfKyCB1FtT9L0EGxsFk5Up8njj0bXK8pJM9+Wq2Nya7/jslmCQwRK39LFm55h7NBTw=="
|
||||||
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||||
|
@ -31713,6 +31765,11 @@
|
||||||
"minimalistic-assert": "^1.0.0"
|
"minimalistic-assert": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"web-streams-polyfill": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-EqPmREeOzttaLRm5HS7io98goBgZ7IVz79aDvqjD0kYXLtFZTc0T/U6wHTPKyIjb+MdN7DFIIX6hgdBEpWmfPA=="
|
||||||
|
},
|
||||||
"web-vitals": {
|
"web-vitals": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-2.1.4.tgz",
|
||||||
|
|
|
@ -12,14 +12,18 @@
|
||||||
"@types/react": "^17.0.39",
|
"@types/react": "^17.0.39",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^17.0.11",
|
||||||
"@types/react-router-bootstrap": "^0.24.5",
|
"@types/react-router-bootstrap": "^0.24.5",
|
||||||
|
"@types/streamsaver": "^2.0.1",
|
||||||
"bootstrap": "^5.1.3",
|
"bootstrap": "^5.1.3",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-bootstrap": "^2.1.2",
|
"react-bootstrap": "^2.1.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
"react-icons": "^4.3.1",
|
||||||
"react-router-bootstrap": "^0.26.0",
|
"react-router-bootstrap": "^0.26.0",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.2.1",
|
||||||
"react-scripts": "5.0.0",
|
"react-scripts": "5.0.0",
|
||||||
|
"streamsaver": "^2.0.6",
|
||||||
"typescript": "^4.5.5",
|
"typescript": "^4.5.5",
|
||||||
|
"web-streams-polyfill": "^3.2.0",
|
||||||
"web-vitals": "^2.1.4"
|
"web-vitals": "^2.1.4"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
@ -27,7 +27,7 @@ class BucketList extends React.Component<Props, State> {
|
||||||
const data = await this.props.client.send(command);
|
const data = await this.props.client.send(command);
|
||||||
console.log("ok", data);
|
console.log("ok", data);
|
||||||
const buckets = (data.Buckets || []).map((b) => (b.Name || 'aza'));
|
const buckets = (data.Buckets || []).map((b) => (b.Name || 'aza'));
|
||||||
buckets.sort();
|
buckets.sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}));
|
||||||
this.setState({buckets: buckets});
|
this.setState({buckets: buckets});
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
console.log("err", error);
|
console.log("err", error);
|
||||||
|
|
84
src/ObjectInfo.tsx
Normal file
84
src/ObjectInfo.tsx
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import { S3Client, ListObjectsV2Command, HeadObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
import Button from 'react-bootstrap/Button';
|
||||||
|
import Modal from 'react-bootstrap/Modal';
|
||||||
|
import { BsFileEarmarkText, BsDownload } from 'react-icons/bs';
|
||||||
|
|
||||||
|
import downloadFile from './downloadFile';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
client: S3Client;
|
||||||
|
bucket: string;
|
||||||
|
s3key: string;
|
||||||
|
filename: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
show: boolean;
|
||||||
|
loaded: boolean;
|
||||||
|
size: number;
|
||||||
|
contentType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ObjectInfo extends React.Component<Props, State> {
|
||||||
|
state = {
|
||||||
|
show: true,
|
||||||
|
loaded: false,
|
||||||
|
size: 0,
|
||||||
|
contentType: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
let command = new HeadObjectCommand({
|
||||||
|
Bucket: this.props.bucket,
|
||||||
|
Key: this.props.s3key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await this.props.client.send(command);
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
loaded: true,
|
||||||
|
size: data.ContentLength!,
|
||||||
|
contentType: data.ContentType || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
this.setState({show: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <>
|
||||||
|
<Modal show={this.state.show} onHide={() => this.handleClose()}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title><BsFileEarmarkText /> { this.props.filename }</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<p><strong>Bucket:</strong> { this.props.bucket }</p>
|
||||||
|
<p><strong>Key:</strong> { this.props.s3key }</p>
|
||||||
|
{ this.state.loaded ?
|
||||||
|
<>
|
||||||
|
<p><strong>Size:</strong> { this.state.size }</p>
|
||||||
|
<p><strong>Content type:</strong> { this.state.contentType }</p>
|
||||||
|
</>
|
||||||
|
: <></> }
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="primary" onClick={() =>
|
||||||
|
downloadFile(this.props.client, this.props.bucket, this.props.s3key, this.props.filename)
|
||||||
|
}>
|
||||||
|
<BsDownload /> Download
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ObjectInfo;
|
|
@ -1,13 +1,22 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link, useParams } from 'react-router-dom';
|
import { Link, useParams } from 'react-router-dom';
|
||||||
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
|
import { S3Client, ListObjectsV2Command, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
import Alert from 'react-bootstrap/Alert';
|
import Alert from 'react-bootstrap/Alert';
|
||||||
import Breadcrumb from 'react-bootstrap/Breadcrumb';
|
import Breadcrumb from 'react-bootstrap/Breadcrumb';
|
||||||
|
import Button from 'react-bootstrap/Button';
|
||||||
|
import Container from 'react-bootstrap/Container';
|
||||||
import Card from 'react-bootstrap/Card';
|
import Card from 'react-bootstrap/Card';
|
||||||
import ListGroup from 'react-bootstrap/ListGroup';
|
import ListGroup from 'react-bootstrap/ListGroup';
|
||||||
|
import Stack from 'react-bootstrap/Stack';
|
||||||
import { LinkContainer } from 'react-router-bootstrap'
|
import { LinkContainer } from 'react-router-bootstrap'
|
||||||
|
|
||||||
|
import ObjectInfo from './ObjectInfo';
|
||||||
|
import UploadFiles from './UploadFiles';
|
||||||
|
import downloadFile from './downloadFile';
|
||||||
|
|
||||||
|
import { BsFolder, BsFileEarmarkText, BsDownload } from 'react-icons/bs';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
client: S3Client;
|
client: S3Client;
|
||||||
bucket: string;
|
bucket: string;
|
||||||
|
@ -18,6 +27,9 @@ type State = {
|
||||||
loaded: boolean;
|
loaded: boolean;
|
||||||
folders: string[];
|
folders: string[];
|
||||||
files: string[];
|
files: string[];
|
||||||
|
info: string | null;
|
||||||
|
iInfo: number;
|
||||||
|
iUpload: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
var cache: { [path: string]: State; } = {};
|
var cache: { [path: string]: State; } = {};
|
||||||
|
@ -27,19 +39,26 @@ class ObjectList extends React.Component<Props, State> {
|
||||||
loaded: false,
|
loaded: false,
|
||||||
folders: [],
|
folders: [],
|
||||||
files: [],
|
files: [],
|
||||||
|
info: null,
|
||||||
|
iInfo: 0,
|
||||||
|
iUpload: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props: Props) {
|
constructor(props: Props) {
|
||||||
super(props);
|
super(props);
|
||||||
}
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
componentDidMount() {
|
||||||
console.log(this.props);
|
//console.log(this.props);
|
||||||
|
|
||||||
if (cache[this.path()]) {
|
if (cache[this.path()]) {
|
||||||
this.setState(cache[this.path()]);
|
this.setState(cache[this.path()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadEntries() {
|
||||||
let command = new ListObjectsV2Command({
|
let command = new ListObjectsV2Command({
|
||||||
Bucket: this.props.bucket,
|
Bucket: this.props.bucket,
|
||||||
Prefix: this.props.prefix,
|
Prefix: this.props.prefix,
|
||||||
|
@ -49,13 +68,13 @@ class ObjectList extends React.Component<Props, State> {
|
||||||
const pxlen = this.props.prefix.length;
|
const pxlen = this.props.prefix.length;
|
||||||
|
|
||||||
const data = await this.props.client.send(command);
|
const data = await this.props.client.send(command);
|
||||||
console.log("ok", data);
|
//console.log("ok", data);
|
||||||
|
|
||||||
const folders = (data.CommonPrefixes || []).map((cp) => cp.Prefix!.substring(pxlen));
|
const folders = (data.CommonPrefixes || []).map((cp) => cp.Prefix!.substring(pxlen));
|
||||||
const files = (data.Contents || []).map((obj) => obj.Key!.substring(pxlen));
|
const files = (data.Contents || []).map((obj) => obj.Key!.substring(pxlen));
|
||||||
|
|
||||||
folders.sort();
|
folders.sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}));
|
||||||
files.sort();
|
files.sort((a, b) => a.localeCompare(b, undefined, {sensitivity: 'base'}));
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loaded: true,
|
loaded: true,
|
||||||
|
@ -76,27 +95,28 @@ class ObjectList extends React.Component<Props, State> {
|
||||||
let spl = this.props.prefix.split("/");
|
let spl = this.props.prefix.split("/");
|
||||||
let items = [];
|
let items = [];
|
||||||
for (var i = 0; i < spl.length - 1; i++) {
|
for (var i = 0; i < spl.length - 1; i++) {
|
||||||
|
let to = "/" + this.props.bucket + "/" + spl.slice(0, i+1).join("/") + "/" ;
|
||||||
if (i < spl.length - 2) {
|
if (i < spl.length - 2) {
|
||||||
items.push(
|
items.push(
|
||||||
<LinkContainer to={ "/" + this.props.bucket + "/" + spl.slice(0, i+1).join("/") + "/" }>
|
<LinkContainer to={to} key={to}>
|
||||||
<Breadcrumb.Item>{ spl[i] }</Breadcrumb.Item>
|
<Breadcrumb.Item>{ spl[i] }</Breadcrumb.Item>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
items.push(
|
items.push(
|
||||||
<Breadcrumb.Item active>{ spl[i] }</Breadcrumb.Item>
|
<Breadcrumb.Item active key={to}>{ spl[i] }</Breadcrumb.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Breadcrumb>
|
<Breadcrumb>
|
||||||
<LinkContainer to="/">
|
<LinkContainer key="BUCKETS" to="/">
|
||||||
<Breadcrumb.Item>my buckets</Breadcrumb.Item>
|
<Breadcrumb.Item>my buckets</Breadcrumb.Item>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
{ this.props.prefix == "" ?
|
{ this.props.prefix == "" ?
|
||||||
<Breadcrumb.Item active>{ this.props.bucket }</Breadcrumb.Item>
|
<Breadcrumb.Item active>{ this.props.bucket }</Breadcrumb.Item>
|
||||||
:
|
:
|
||||||
<LinkContainer to={ "/" + this.props.bucket + "/" }>
|
<LinkContainer key="BUCKET" to={ "/" + this.props.bucket + "/" }>
|
||||||
<Breadcrumb.Item>{ this.props.bucket }</Breadcrumb.Item>
|
<Breadcrumb.Item>{ this.props.bucket }</Breadcrumb.Item>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
}
|
}
|
||||||
|
@ -105,8 +125,19 @@ class ObjectList extends React.Component<Props, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
openInfo(f: string) {
|
||||||
|
this.setState({info: f, iInfo: this.state.iInfo + 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
openUpload() {
|
||||||
|
this.setState({iUpload: this.state.iUpload + 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUploadComplete() {
|
||||||
|
this.loadEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
if (!this.state.loaded) {
|
if (!this.state.loaded) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -118,18 +149,56 @@ class ObjectList extends React.Component<Props, State> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{ this.state.iUpload > 0 ?
|
||||||
|
<UploadFiles
|
||||||
|
key={ "upload" + this.state.iUpload }
|
||||||
|
client={this.props.client}
|
||||||
|
bucket={this.props.bucket}
|
||||||
|
prefix={this.props.prefix}
|
||||||
|
onUploadComplete={() => this.onUploadComplete()}
|
||||||
|
/>
|
||||||
|
: <></> }
|
||||||
|
{ this.state.info ?
|
||||||
|
<ObjectInfo
|
||||||
|
key={ "info" + this.state.iInfo }
|
||||||
|
client={ this.props.client }
|
||||||
|
bucket={this.props.bucket}
|
||||||
|
s3key={this.props.prefix + this.state.info}
|
||||||
|
filename={this.state.info} />
|
||||||
|
: <></> }
|
||||||
|
<Container className="pb-3">
|
||||||
|
<Stack direction="horizontal">
|
||||||
|
<div className="mt-1">
|
||||||
{ this.renderBreadcrumbs() }
|
{ this.renderBreadcrumbs() }
|
||||||
|
</div>
|
||||||
|
<div className="ms-auto">
|
||||||
|
<Button size="sm" variant="info" onClick={(event) => this.openUpload()}>
|
||||||
|
Upload files
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
<ListGroup>
|
<ListGroup>
|
||||||
{ this.state.folders.map((f) =>
|
{ this.state.folders.map((f) =>
|
||||||
<LinkContainer to={ "/" + this.props.bucket + "/" + this.props.prefix + f }>
|
<LinkContainer key={f + "/"} to={ "/" + this.props.bucket + "/" + this.props.prefix + f }>
|
||||||
<ListGroup.Item key={f + "/"} action>
|
<ListGroup.Item action>
|
||||||
{ f }
|
<BsFolder /> { f }
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
</LinkContainer>
|
</LinkContainer>
|
||||||
)}
|
)}
|
||||||
{ this.state.files.map((f) =>
|
{ this.state.files.map((f) =>
|
||||||
<ListGroup.Item key={f}>
|
<ListGroup.Item key={f} action onClick={() => this.openInfo(f)}>
|
||||||
{ f }
|
<Stack direction="horizontal">
|
||||||
|
<div><BsFileEarmarkText /> { f }</div>
|
||||||
|
<div className="ms-auto">
|
||||||
|
<Button size="sm" variant="info" onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
downloadFile(this.props.client, this.props.bucket, this.props.prefix + f, f);
|
||||||
|
}}>
|
||||||
|
<BsDownload />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
</ListGroup.Item>
|
</ListGroup.Item>
|
||||||
)}
|
)}
|
||||||
</ListGroup>
|
</ListGroup>
|
||||||
|
@ -139,6 +208,11 @@ class ObjectList extends React.Component<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function isBlob(obj: any): obj is Blob {
|
||||||
|
return !! obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
interface IClient {
|
interface IClient {
|
||||||
client: S3Client;
|
client: S3Client;
|
||||||
}
|
}
|
||||||
|
|
163
src/UploadFiles.tsx
Normal file
163
src/UploadFiles.tsx
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Link, useParams } from 'react-router-dom';
|
||||||
|
import { S3Client, ListObjectsV2Command, HeadObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
import Button from 'react-bootstrap/Button';
|
||||||
|
import ListGroup from 'react-bootstrap/ListGroup';
|
||||||
|
import Modal from 'react-bootstrap/Modal';
|
||||||
|
import Stack from 'react-bootstrap/Stack';
|
||||||
|
|
||||||
|
import { BsFileEarmarkText, BsDownload, BsHourglassSplit, BsCheckCircle } from 'react-icons/bs';
|
||||||
|
import { AiOutlineDelete } from 'react-icons/ai';
|
||||||
|
|
||||||
|
import downloadFile from './downloadFile';
|
||||||
|
import fileSizePretty from './fileSizePretty';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
client: S3Client;
|
||||||
|
bucket: string;
|
||||||
|
prefix: string;
|
||||||
|
onUploadComplete: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
iFile: number;
|
||||||
|
show: boolean;
|
||||||
|
files: File[];
|
||||||
|
processing: boolean;
|
||||||
|
canceled: boolean;
|
||||||
|
progressEach: number[];
|
||||||
|
};
|
||||||
|
|
||||||
|
class UploadFiles extends React.Component<Props, State> {
|
||||||
|
file_input: HTMLInputElement | null = null;
|
||||||
|
|
||||||
|
state = {
|
||||||
|
iFile: 0,
|
||||||
|
show: true,
|
||||||
|
files: [],
|
||||||
|
processing: false,
|
||||||
|
canceled: false,
|
||||||
|
progressEach: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
async componentDidMount() {
|
||||||
|
this.addFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClose() {
|
||||||
|
this.setState({show: false});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFileChange() {
|
||||||
|
let infiles = this.file_input!.files || [];
|
||||||
|
let files: File[] = this.state.files;
|
||||||
|
|
||||||
|
for (var i = 0; i < infiles.length; i++) {
|
||||||
|
files.push(infiles[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({show: (files.length > 0), files: files, iFile: this.state.iFile + 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
addFile() {
|
||||||
|
this.file_input!.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFile(i: number) {
|
||||||
|
let files: File[] = this.state.files;
|
||||||
|
files.splice(i, 1);
|
||||||
|
this.setState({files: files});
|
||||||
|
}
|
||||||
|
|
||||||
|
async processUpload() {
|
||||||
|
this.setState({canceled: false});
|
||||||
|
|
||||||
|
let progressEach: number[] = [];
|
||||||
|
for (var i = 0; i < this.state.files.length; i++) {
|
||||||
|
progressEach.push(0);
|
||||||
|
}
|
||||||
|
this.setState({canceled: false, processing: true, progressEach: progressEach});
|
||||||
|
|
||||||
|
for (var i = 0; i < this.state.files.length; i++) {
|
||||||
|
let file: File = this.state.files[i];
|
||||||
|
let req = new PutObjectCommand({
|
||||||
|
Bucket: this.props.bucket,
|
||||||
|
Key: this.props.prefix + file.name,
|
||||||
|
ContentType: file.type,
|
||||||
|
ContentLength: file.size,
|
||||||
|
Body: file,
|
||||||
|
});
|
||||||
|
let resp = await this.props.client.send(req);
|
||||||
|
progressEach[i] = 100;
|
||||||
|
this.setState({progressEach: progressEach});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({show: false});
|
||||||
|
this.props.onUploadComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <>
|
||||||
|
<Modal show={this.state.show} onHide={() => this.handleClose()}>
|
||||||
|
<Modal.Header closeButton>
|
||||||
|
<Modal.Title>
|
||||||
|
Upload files
|
||||||
|
</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<input
|
||||||
|
key={ "file" + this.state.iFile }
|
||||||
|
type="file" id="file_input"
|
||||||
|
ref={input => this.file_input = input}
|
||||||
|
onChange={() => this.handleFileChange()}
|
||||||
|
multiple
|
||||||
|
hidden />
|
||||||
|
{ this.state.files.length == 0 ? <p>No files added yet, click "add file" below.</p> : <></> }
|
||||||
|
<ListGroup>
|
||||||
|
{ this.state.files.map((f: File, i: number) =>
|
||||||
|
<ListGroup.Item>
|
||||||
|
<Stack direction="horizontal">
|
||||||
|
<div className="mt-1">
|
||||||
|
<BsFileEarmarkText /> { f.name }
|
||||||
|
</div>
|
||||||
|
<div className="ms-auto">
|
||||||
|
{ this.state.processing ?
|
||||||
|
(this.state.progressEach[i] == 100 ? <BsCheckCircle />
|
||||||
|
: this.state.progressEach[i] > 0 ? this.state.progressEach[i] + "%"
|
||||||
|
: <BsHourglassSplit /> )
|
||||||
|
:
|
||||||
|
<Button variant="danger" size="sm" onClick={() => this.removeFile(i)}>
|
||||||
|
<AiOutlineDelete />
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="horizontal">
|
||||||
|
<div className="px-2"> </div>
|
||||||
|
<div className="small">
|
||||||
|
{ fileSizePretty(f.size) } - { f.type }
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
</ListGroup.Item>
|
||||||
|
)}
|
||||||
|
</ListGroup>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant="success" onClick={() => this.addFile()}>
|
||||||
|
Add file
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={() => this.processUpload()} disabled={this.state.files.length == 0}>
|
||||||
|
Upload
|
||||||
|
</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UploadFiles;
|
34
src/downloadFile.tsx
Normal file
34
src/downloadFile.tsx
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import StreamSaver from 'streamsaver';
|
||||||
|
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
|
||||||
|
|
||||||
|
|
||||||
|
async function downloadFile(client: S3Client, bucket: string, key: string, filename: string) {
|
||||||
|
let command = new GetObjectCommand({
|
||||||
|
Bucket: bucket,
|
||||||
|
Key: key,
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await client.send(command);
|
||||||
|
|
||||||
|
console.log("body:", data.Body);
|
||||||
|
|
||||||
|
const fileStream = StreamSaver.createWriteStream(filename);
|
||||||
|
const writer = fileStream.getWriter();
|
||||||
|
|
||||||
|
const reader = (data.Body! as ReadableStream).getReader();
|
||||||
|
|
||||||
|
const pump = (): Promise<null|undefined> => reader.read()
|
||||||
|
.then(({ value, done }) => {
|
||||||
|
if (done) writer.close();
|
||||||
|
else {
|
||||||
|
writer.write(value);
|
||||||
|
return writer.ready.then(pump);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await pump()
|
||||||
|
.then(() => console.log('Closed the stream, Done writing'))
|
||||||
|
.catch(err => console.log(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default downloadFile;
|
12
src/fileSizePretty.tsx
Normal file
12
src/fileSizePretty.tsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
function fileSizePretty(nBytes: number): string {
|
||||||
|
let sOutput = nBytes + " bytes";
|
||||||
|
// optional code for multiples approximation
|
||||||
|
const aMultiples = ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
|
||||||
|
for (var nMultiple = 0, nApprox = nBytes / 1024; nApprox > 1; nApprox /= 1024, nMultiple++) {
|
||||||
|
sOutput = nApprox.toFixed(3) + " " + aMultiples[nMultiple] + " (" + nBytes + " bytes)";
|
||||||
|
}
|
||||||
|
return sOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fileSizePretty;
|
|
@ -1,10 +1,12 @@
|
||||||
|
import 'web-streams-polyfill/dist/polyfill.js';
|
||||||
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import './index.css';
|
import './index.css';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
|
|
Loading…
Reference in a new issue