mirror of
https://github.com/deuxfleurs-org/garage.git
synced 2026-08-17 09:07:48 +00:00
Compare commits
20 Commits
v2.0.0-rc1
...
v1.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a4afc04a9 | |||
| fbf03e9378 | |||
| 9eb07d4c7b | |||
| 85ee4f5d8c | |||
| 328072d122 | |||
| 26bc807905 | |||
| a9f5f242b2 | |||
| ae98abca5c | |||
| adfa44ad70 | |||
| 47143b88ad | |||
| 8843aa92fa | |||
| b601b3e46d | |||
| a19d2f16e2 | |||
| fc8fc60f6d | |||
| 77079a1498 | |||
| 2a4f729b57 | |||
| 1b042e379e | |||
| ffbce0f689 | |||
| 37e5621dde | |||
| 6529ff379a |
Generated
+27
-15
@@ -867,9 +867,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-channel"
|
name = "crossbeam-channel"
|
||||||
version = "0.5.14"
|
version = "0.5.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
|
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
@@ -1200,7 +1200,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage"
|
name = "garage"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"assert-json-diff",
|
"assert-json-diff",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1250,12 +1250,13 @@ dependencies = [
|
|||||||
"timeago",
|
"timeago",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-journald",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_api_admin"
|
name = "garage_api_admin"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1281,7 +1282,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_api_common"
|
name = "garage_api_common"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1315,7 +1316,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_api_k2v"
|
name = "garage_api_k2v"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"err-derive",
|
"err-derive",
|
||||||
@@ -1338,7 +1339,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_api_s3"
|
name = "garage_api_s3"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -1383,7 +1384,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_block"
|
name = "garage_block"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-compression",
|
"async-compression",
|
||||||
@@ -1408,7 +1409,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_db"
|
name = "garage_db"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"err-derive",
|
"err-derive",
|
||||||
"heed",
|
"heed",
|
||||||
@@ -1421,7 +1422,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_model"
|
name = "garage_model"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
@@ -1448,7 +1449,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_net"
|
name = "garage_net"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1473,7 +1474,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_rpc"
|
name = "garage_rpc"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1505,7 +1506,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_table"
|
name = "garage_table"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1526,7 +1527,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_util"
|
name = "garage_util"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -1558,7 +1559,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "garage_web"
|
name = "garage_web"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"err-derive",
|
"err-derive",
|
||||||
"garage_api_common",
|
"garage_api_common",
|
||||||
@@ -4514,6 +4515,17 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-journald"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc0b4143302cf1022dac868d521e36e8b27691f72c84b3311750d5188ebba657"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"tracing-core",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-log"
|
name = "tracing-log"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
+13
-12
@@ -24,18 +24,18 @@ default-members = ["src/garage"]
|
|||||||
|
|
||||||
# Internal Garage crates
|
# Internal Garage crates
|
||||||
format_table = { version = "0.1.1", path = "src/format-table" }
|
format_table = { version = "0.1.1", path = "src/format-table" }
|
||||||
garage_api_common = { version = "1.1.0", path = "src/api/common" }
|
garage_api_common = { version = "1.2.0", path = "src/api/common" }
|
||||||
garage_api_admin = { version = "1.1.0", path = "src/api/admin" }
|
garage_api_admin = { version = "1.2.0", path = "src/api/admin" }
|
||||||
garage_api_s3 = { version = "1.1.0", path = "src/api/s3" }
|
garage_api_s3 = { version = "1.2.0", path = "src/api/s3" }
|
||||||
garage_api_k2v = { version = "1.1.0", path = "src/api/k2v" }
|
garage_api_k2v = { version = "1.2.0", path = "src/api/k2v" }
|
||||||
garage_block = { version = "1.1.0", path = "src/block" }
|
garage_block = { version = "1.2.0", path = "src/block" }
|
||||||
garage_db = { version = "1.1.0", path = "src/db", default-features = false }
|
garage_db = { version = "1.2.0", path = "src/db", default-features = false }
|
||||||
garage_model = { version = "1.1.0", path = "src/model", default-features = false }
|
garage_model = { version = "1.2.0", path = "src/model", default-features = false }
|
||||||
garage_net = { version = "1.1.0", path = "src/net" }
|
garage_net = { version = "1.2.0", path = "src/net" }
|
||||||
garage_rpc = { version = "1.1.0", path = "src/rpc" }
|
garage_rpc = { version = "1.2.0", path = "src/rpc" }
|
||||||
garage_table = { version = "1.1.0", path = "src/table" }
|
garage_table = { version = "1.2.0", path = "src/table" }
|
||||||
garage_util = { version = "1.1.0", path = "src/util" }
|
garage_util = { version = "1.2.0", path = "src/util" }
|
||||||
garage_web = { version = "1.1.0", path = "src/web" }
|
garage_web = { version = "1.2.0", path = "src/web" }
|
||||||
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
k2v-client = { version = "0.0.4", path = "src/k2v-client" }
|
||||||
|
|
||||||
# External crates from crates.io
|
# External crates from crates.io
|
||||||
@@ -83,6 +83,7 @@ pretty_env_logger = "0.5"
|
|||||||
structopt = { version = "0.3", default-features = false }
|
structopt = { version = "0.3", default-features = false }
|
||||||
syslog-tracing = "0.3"
|
syslog-tracing = "0.3"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
|
tracing-journald = "0.3.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
heed = { version = "0.11", default-features = false, features = ["lmdb"] }
|
heed = { version = "0.11", default-features = false, features = ["lmdb"] }
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ have published Ansible roles. We list them and compare them below.
|
|||||||
|
|
||||||
## Comparison of Ansible roles
|
## Comparison of Ansible roles
|
||||||
|
|
||||||
| Feature | [ansible-role-garage](#zorun-ansible-role-garage) | [garage-docker-ansible-deploy](#moan0s-garage-docker-ansible-deploy) |
|
| Feature | [ansible-role-garage](#zorun-ansible-role-garage) | [garage-docker-ansible-deploy](#moan0s-garage-docker-ansible-deploy) | [eddster ansible-role-garage](#eddster-ansible-role-garage) |
|
||||||
|------------------------------------|---------------------------------------------|---------------------------------------------------------------|
|
|------------------------------------|---------------------------------------------|---------------------------------------------------------------|---------------------------------|
|
||||||
| **Runtime** | Systemd | Docker |
|
| **Runtime** | Systemd | Docker | Systemd |
|
||||||
| **Target OS** | Any Linux | Any Linux |
|
| **Target OS** | Any Linux | Any Linux | Any Linux |
|
||||||
| **Architecture** | amd64, arm64, i686 | amd64, arm64 |
|
| **Architecture** | amd64, arm64, i686 | amd64, arm64 | arm64, arm, 386, amd64 |
|
||||||
| **Additional software** | None | Traefik |
|
| **Additional software** | None | Traefik | Ngnix and Keepalived (optional) |
|
||||||
| **Automatic node connection** | ❌ | ✅ |
|
| **Automatic node connection** | ❌ | ✅ | ✅ |
|
||||||
| **Layout management** | ❌ | ✅ |
|
| **Layout management** | ❌ | ✅ | ✅ |
|
||||||
| **Manage buckets & keys** | ❌ | ✅ (basic) |
|
| **Manage buckets & keys** | ❌ | ✅ (basic) | ✅ |
|
||||||
| **Allow custom Garage config** | ✅ | ❌ |
|
| **Allow custom Garage config** | ✅ | ❌ | ❌ |
|
||||||
| **Facilitate Garage upgrades** | ✅ | ❌ |
|
| **Facilitate Garage upgrades** | ✅ | ❌ | ✅ |
|
||||||
| **Multiple instances on one host** | ✅ | ✅ |
|
| **Multiple instances on one host** | ✅ | ✅ | ❌ |
|
||||||
|
|
||||||
|
|
||||||
## zorun/ansible-role-garage
|
## zorun/ansible-role-garage
|
||||||
@@ -49,3 +49,15 @@ structured DNS names, etc).
|
|||||||
|
|
||||||
As a result, this role makes it easier to start with Garage on Ansible,
|
As a result, this role makes it easier to start with Garage on Ansible,
|
||||||
but is less flexible.
|
but is less flexible.
|
||||||
|
|
||||||
|
## eddster2309/ansible-role-garage
|
||||||
|
|
||||||
|
[Source code](https://github.com/eddster2309/ansible-role-garage), [Ansible galaxy](https://galaxy.ansible.com/ui/standalone/roles/eddster2309/garage/)
|
||||||
|
|
||||||
|
This role is a opinionated but customisable role using the official Garage
|
||||||
|
static binaries and only requires Systemd. As such it should work on any
|
||||||
|
Linux based host. It includes all the nesscary configuration to
|
||||||
|
automatically setup a clustered Garage deployment. Most Garage
|
||||||
|
configuration options are exposed through Ansible variables so while you
|
||||||
|
can't provide a custom config you can get very close. It can optionally
|
||||||
|
installed a HA nginx deployment with Keepalived.
|
||||||
|
|||||||
@@ -96,14 +96,14 @@ to store 2 TB of data in total.
|
|||||||
## Get a Docker image
|
## Get a Docker image
|
||||||
|
|
||||||
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
Our docker image is currently named `dxflrs/garage` and is stored on the [Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||||
We encourage you to use a fixed tag (eg. `v1.1.0`) and not the `latest` tag.
|
We encourage you to use a fixed tag (eg. `v1.2.0`) and not the `latest` tag.
|
||||||
For this example, we will use the latest published version at the time of the writing which is `v1.1.0` but it's up to you
|
For this example, we will use the latest published version at the time of the writing which is `v1.2.0` but it's up to you
|
||||||
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
to check [the most recent versions on the Docker Hub](https://hub.docker.com/r/dxflrs/garage/tags?page=1&ordering=last_updated).
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo docker pull dxflrs/garage:v1.1.0
|
sudo docker pull dxflrs/garage:v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
## Deploying and configuring Garage
|
## Deploying and configuring Garage
|
||||||
@@ -171,7 +171,7 @@ docker run \
|
|||||||
-v /etc/garage.toml:/etc/garage.toml \
|
-v /etc/garage.toml:/etc/garage.toml \
|
||||||
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
-v /var/lib/garage/meta:/var/lib/garage/meta \
|
||||||
-v /var/lib/garage/data:/var/lib/garage/data \
|
-v /var/lib/garage/data:/var/lib/garage/data \
|
||||||
dxflrs/garage:v1.1.0
|
dxflrs/garage:v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
With this command line, Garage should be started automatically at each boot.
|
With this command line, Garage should be started automatically at each boot.
|
||||||
@@ -185,7 +185,7 @@ If you want to use `docker-compose`, you may use the following `docker-compose.y
|
|||||||
version: "3"
|
version: "3"
|
||||||
services:
|
services:
|
||||||
garage:
|
garage:
|
||||||
image: dxflrs/garage:v1.1.0
|
image: dxflrs/garage:v1.2.0
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ docker run \
|
|||||||
-v /path/to/garage.toml:/etc/garage.toml \
|
-v /path/to/garage.toml:/etc/garage.toml \
|
||||||
-v /path/to/garage/meta:/var/lib/garage/meta \
|
-v /path/to/garage/meta:/var/lib/garage/meta \
|
||||||
-v /path/to/garage/data:/var/lib/garage/data \
|
-v /path/to/garage/data:/var/lib/garage/data \
|
||||||
dxflrs/garage:v1.1.0
|
dxflrs/garage:v1.2.0
|
||||||
```
|
```
|
||||||
|
|
||||||
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
|
Under Linux, you can substitute `--network host` for `-p 3900:3900 -p 3901:3901 -p 3902:3902 -p 3903:3903`
|
||||||
@@ -182,11 +182,12 @@ ID Hostname Address Tag Zone Capacit
|
|||||||
## Creating a cluster layout
|
## Creating a cluster layout
|
||||||
|
|
||||||
Creating a cluster layout for a Garage deployment means informing Garage
|
Creating a cluster layout for a Garage deployment means informing Garage
|
||||||
of the disk space available on each node of the cluster
|
of the disk space available on each node of the cluster, `-c`,
|
||||||
as well as the zone (e.g. datacenter) each machine is located in.
|
as well as the name of the zone (e.g. datacenter), `-z`, each machine is located in.
|
||||||
|
|
||||||
For our test deployment, we are using only one node. The way in which we configure
|
For our test deployment, we are have only one node with zone named `dc1` and a
|
||||||
it does not matter, you can simply write:
|
capacity of `1G`, though the capacity is ignored for a single node deployment
|
||||||
|
and can be changed later when adding new nodes.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
garage layout assign -z dc1 -c 1G <node_id>
|
garage layout assign -z dc1 -c 1G <node_id>
|
||||||
|
|||||||
@@ -93,30 +93,30 @@ The following gives details about each available configuration option.
|
|||||||
|
|
||||||
[Environment variables](#env_variables).
|
[Environment variables](#env_variables).
|
||||||
|
|
||||||
Top-level configuration options:
|
Top-level configuration options, in alphabetical order:
|
||||||
|
[`allow_punycode`](#allow_punycode),
|
||||||
[`allow_world_readable_secrets`](#allow_world_readable_secrets),
|
[`allow_world_readable_secrets`](#allow_world_readable_secrets),
|
||||||
[`block_ram_buffer_max`](#block_ram_buffer_max),
|
[`block_ram_buffer_max`](#block_ram_buffer_max),
|
||||||
[`block_size`](#block_size),
|
[`block_size`](#block_size),
|
||||||
[`bootstrap_peers`](#bootstrap_peers),
|
[`bootstrap_peers`](#bootstrap_peers),
|
||||||
[`compression_level`](#compression_level),
|
[`compression_level`](#compression_level),
|
||||||
|
[`consistency_mode`](#consistency_mode),
|
||||||
[`data_dir`](#data_dir),
|
[`data_dir`](#data_dir),
|
||||||
[`data_fsync`](#data_fsync),
|
[`data_fsync`](#data_fsync),
|
||||||
[`db_engine`](#db_engine),
|
[`db_engine`](#db_engine),
|
||||||
[`disable_scrub`](#disable_scrub),
|
[`disable_scrub`](#disable_scrub),
|
||||||
[`use_local_tz`](#use_local_tz),
|
|
||||||
[`lmdb_map_size`](#lmdb_map_size),
|
[`lmdb_map_size`](#lmdb_map_size),
|
||||||
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval),
|
[`metadata_auto_snapshot_interval`](#metadata_auto_snapshot_interval),
|
||||||
[`metadata_dir`](#metadata_dir),
|
[`metadata_dir`](#metadata_dir),
|
||||||
[`metadata_fsync`](#metadata_fsync),
|
[`metadata_fsync`](#metadata_fsync),
|
||||||
[`metadata_snapshots_dir`](#metadata_snapshots_dir),
|
[`metadata_snapshots_dir`](#metadata_snapshots_dir),
|
||||||
[`replication_factor`](#replication_factor),
|
[`replication_factor`](#replication_factor),
|
||||||
[`consistency_mode`](#consistency_mode),
|
|
||||||
[`rpc_bind_addr`](#rpc_bind_addr),
|
[`rpc_bind_addr`](#rpc_bind_addr),
|
||||||
[`rpc_bind_outgoing`](#rpc_bind_outgoing),
|
[`rpc_bind_outgoing`](#rpc_bind_outgoing),
|
||||||
[`rpc_public_addr`](#rpc_public_addr),
|
[`rpc_public_addr`](#rpc_public_addr),
|
||||||
[`rpc_public_addr_subnet`](#rpc_public_addr_subnet)
|
[`rpc_public_addr_subnet`](#rpc_public_addr_subnet)
|
||||||
[`rpc_secret`/`rpc_secret_file`](#rpc_secret).
|
[`rpc_secret`/`rpc_secret_file`](#rpc_secret),
|
||||||
[`allow_punycode`](#allow_punycode).
|
[`use_local_tz`](#use_local_tz).
|
||||||
|
|
||||||
The `[consul_discovery]` section:
|
The `[consul_discovery]` section:
|
||||||
[`api`](#consul_api),
|
[`api`](#consul_api),
|
||||||
@@ -153,13 +153,17 @@ The `[admin]` section:
|
|||||||
|
|
||||||
### Environment variables {#env_variables}
|
### Environment variables {#env_variables}
|
||||||
|
|
||||||
The following configuration parameter must be specified as an environment
|
The following configuration parameters must be specified as environment variables,
|
||||||
variable, it does not exist in the configuration file:
|
they do not exist in the configuration file:
|
||||||
|
|
||||||
- `GARAGE_LOG_TO_SYSLOG` (since `v0.9.4`): set this to `1` or `true` to make the
|
- `GARAGE_LOG_TO_SYSLOG` (since `v0.9.4`): set this to `1` or `true` to make the
|
||||||
Garage daemon send its logs to `syslog` (using the libc `syslog` function)
|
Garage daemon send its logs to `syslog` (using the libc `syslog` function)
|
||||||
instead of printing to stderr.
|
instead of printing to stderr.
|
||||||
|
|
||||||
|
- `GARAGE_LOG_TO_JOURNALD` (since `v1.2.0`): set this to `1` or `true` to make the
|
||||||
|
Garage daemon send its logs to `journald` (using the native protocol of `systemd-journald`)
|
||||||
|
instead of printing to stderr.
|
||||||
|
|
||||||
The following environment variables can be used to override the corresponding
|
The following environment variables can be used to override the corresponding
|
||||||
values in the configuration file:
|
values in the configuration file:
|
||||||
|
|
||||||
@@ -171,7 +175,7 @@ values in the configuration file:
|
|||||||
|
|
||||||
### Top-level configuration options
|
### Top-level configuration options
|
||||||
|
|
||||||
#### `replication_factor` {#replication_factor}
|
#### `replication_factor` (since `v1.0.0`) {#replication_factor}
|
||||||
|
|
||||||
The replication factor can be any positive integer smaller or equal the node count in your cluster.
|
The replication factor can be any positive integer smaller or equal the node count in your cluster.
|
||||||
The chosen replication factor has a big impact on the cluster's failure tolerancy and performance characteristics.
|
The chosen replication factor has a big impact on the cluster's failure tolerancy and performance characteristics.
|
||||||
@@ -219,7 +223,7 @@ is in progress. In theory, no data should be lost as rebalancing is a
|
|||||||
routine operation for Garage, although we cannot guarantee you that everything
|
routine operation for Garage, although we cannot guarantee you that everything
|
||||||
will go right in such an extreme scenario.
|
will go right in such an extreme scenario.
|
||||||
|
|
||||||
#### `consistency_mode` {#consistency_mode}
|
#### `consistency_mode` (since `v1.0.0`) {#consistency_mode}
|
||||||
|
|
||||||
The consistency mode setting determines the read and write behaviour of your cluster.
|
The consistency mode setting determines the read and write behaviour of your cluster.
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ Feel free to open a PR to suggest fixes this table. Minio is missing because the
|
|||||||
- 2022-05-25 - Many Ceph S3 endpoints are not documented but implemented. Following a notification from the Ceph community, we added them.
|
- 2022-05-25 - Many Ceph S3 endpoints are not documented but implemented. Following a notification from the Ceph community, we added them.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## High-level features
|
## High-level features
|
||||||
|
|
||||||
| Feature | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) |
|
| Feature | Garage | [Openstack Swift](https://docs.openstack.org/swift/latest/s3_compat.html) | [Ceph Object Gateway](https://docs.ceph.com/en/latest/radosgw/s3/) | [Riak CS](https://docs.riak.com/riak/cs/2.1.1/references/apis/storage/s3/index.html) | [OpenIO](https://docs.openio.io/latest/source/arch-design/s3_compliancy.html) |
|
||||||
@@ -34,6 +33,7 @@ Feel free to open a PR to suggest fixes this table. Minio is missing because the
|
|||||||
| [URL vhost-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access) URL (eg. `bucket.host.tld/key`) | ✅ Implemented | ❌| ✅| ✅ | ✅ |
|
| [URL vhost-style](https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html#virtual-hosted-style-access) URL (eg. `bucket.host.tld/key`) | ✅ Implemented | ❌| ✅| ✅ | ✅ |
|
||||||
| [Presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) | ✅ Implemented | ❌| ✅ | ✅ | ✅(❓) |
|
| [Presigned URLs](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html) | ✅ Implemented | ❌| ✅ | ✅ | ✅(❓) |
|
||||||
| [SSE-C encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html) | ✅ Implemented | ❓ | ✅ | ❌ | ✅ |
|
| [SSE-C encryption](https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerSideEncryptionCustomerKeys.html) | ✅ Implemented | ❓ | ✅ | ❌ | ✅ |
|
||||||
|
| [Bucket versioning](https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html) | ❌ Missing | ✅ | ✅ | ❌ | ✅ |
|
||||||
|
|
||||||
*Note:* OpenIO does not says if it supports presigned URLs. Because it is part
|
*Note:* OpenIO does not says if it supports presigned URLs. Because it is part
|
||||||
of signature v4 and they claim they support it without additional precisions,
|
of signature v4 and they claim they support it without additional precisions,
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ Example response body:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
|
"node": "b10c110e4e854e5aa3f4637681befac755154b20059ec163254ddbfae86b09df",
|
||||||
"garageVersion": "v1.1.0",
|
"garageVersion": "v1.2.0",
|
||||||
"garageFeatures": [
|
"garageFeatures": [
|
||||||
"k2v",
|
"k2v",
|
||||||
"lmdb",
|
"lmdb",
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ let
|
|||||||
"metrics"
|
"metrics"
|
||||||
"telemetry-otlp"
|
"telemetry-otlp"
|
||||||
"syslog"
|
"syslog"
|
||||||
|
"journald"
|
||||||
]));
|
]));
|
||||||
|
|
||||||
featuresStr = lib.concatStringsSep "," rootFeatures;
|
featuresStr = lib.concatStringsSep "," rootFeatures;
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
|||||||
name: garage
|
name: garage
|
||||||
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
description: S3-compatible object store for small self-hosted geo-distributed deployments
|
||||||
type: application
|
type: application
|
||||||
version: 0.7.0
|
version: 0.7.1
|
||||||
appVersion: "v1.1.0"
|
appVersion: "v1.2.0"
|
||||||
home: https://garagehq.deuxfleurs.fr/
|
home: https://garagehq.deuxfleurs.fr/
|
||||||
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
|
icon: https://garagehq.deuxfleurs.fr/images/garage-logo.svg
|
||||||
|
|
||||||
@@ -15,4 +15,4 @@ keywords:
|
|||||||
sources:
|
sources:
|
||||||
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
- https://git.deuxfleurs.fr/Deuxfleurs/garage.git
|
||||||
|
|
||||||
maintainers: []
|
maintainers: []
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# garage
|
# garage
|
||||||
|
|
||||||
  
|
  
|
||||||
|
|
||||||
S3-compatible object store for small self-hosted geo-distributed deployments
|
S3-compatible object store for small self-hosted geo-distributed deployments
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_admin"
|
name = "garage_api_admin"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_common"
|
name = "garage_api_common"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_k2v"
|
name = "garage_api_k2v"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_api_s3"
|
name = "garage_api_s3"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -226,6 +226,7 @@ impl ApiHandler for S3ApiServer {
|
|||||||
Endpoint::DeleteBucket {} => handle_delete_bucket(ctx).await,
|
Endpoint::DeleteBucket {} => handle_delete_bucket(ctx).await,
|
||||||
Endpoint::GetBucketLocation {} => handle_get_bucket_location(ctx),
|
Endpoint::GetBucketLocation {} => handle_get_bucket_location(ctx),
|
||||||
Endpoint::GetBucketVersioning {} => handle_get_bucket_versioning(),
|
Endpoint::GetBucketVersioning {} => handle_get_bucket_versioning(),
|
||||||
|
Endpoint::GetBucketAcl {} => handle_get_bucket_acl(ctx),
|
||||||
Endpoint::ListObjects {
|
Endpoint::ListObjects {
|
||||||
delimiter,
|
delimiter,
|
||||||
encoding_type,
|
encoding_type,
|
||||||
|
|||||||
+59
-1
@@ -5,7 +5,7 @@ use hyper::{Request, Response, StatusCode};
|
|||||||
use garage_model::bucket_alias_table::*;
|
use garage_model::bucket_alias_table::*;
|
||||||
use garage_model::bucket_table::Bucket;
|
use garage_model::bucket_table::Bucket;
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_model::key_table::Key;
|
use garage_model::key_table::{Key, KeyParams};
|
||||||
use garage_model::permission::BucketKeyPerm;
|
use garage_model::permission::BucketKeyPerm;
|
||||||
use garage_table::util::*;
|
use garage_table::util::*;
|
||||||
use garage_util::crdt::*;
|
use garage_util::crdt::*;
|
||||||
@@ -44,6 +44,55 @@ pub fn handle_get_bucket_versioning() -> Result<Response<ResBody>, Error> {
|
|||||||
.body(string_body(xml))?)
|
.body(string_body(xml))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn handle_get_bucket_acl(ctx: ReqCtx) -> Result<Response<ResBody>, Error> {
|
||||||
|
let ReqCtx {
|
||||||
|
bucket_id, api_key, ..
|
||||||
|
} = ctx;
|
||||||
|
let key_p = api_key.params().ok_or_internal_error(
|
||||||
|
"Key should not be in deleted state at this point (in handle_get_bucket_acl)",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut grants: Vec<s3_xml::Grant> = vec![];
|
||||||
|
let kp = api_key.bucket_permissions(&bucket_id);
|
||||||
|
|
||||||
|
if kp.allow_owner {
|
||||||
|
grants.push(s3_xml::Grant {
|
||||||
|
grantee: create_grantee(&key_p, &api_key),
|
||||||
|
permission: s3_xml::Value("FULL_CONTROL".to_string()),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if kp.allow_read {
|
||||||
|
grants.push(s3_xml::Grant {
|
||||||
|
grantee: create_grantee(&key_p, &api_key),
|
||||||
|
permission: s3_xml::Value("READ".to_string()),
|
||||||
|
});
|
||||||
|
grants.push(s3_xml::Grant {
|
||||||
|
grantee: create_grantee(&key_p, &api_key),
|
||||||
|
permission: s3_xml::Value("READ_ACP".to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if kp.allow_write {
|
||||||
|
grants.push(s3_xml::Grant {
|
||||||
|
grantee: create_grantee(&key_p, &api_key),
|
||||||
|
permission: s3_xml::Value("WRITE".to_string()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let access_control_policy = s3_xml::AccessControlPolicy {
|
||||||
|
xmlns: (),
|
||||||
|
owner: None,
|
||||||
|
acl: s3_xml::AccessControlList { entries: grants },
|
||||||
|
};
|
||||||
|
|
||||||
|
let xml = s3_xml::to_xml_with_header(&access_control_policy)?;
|
||||||
|
trace!("xml: {}", xml);
|
||||||
|
|
||||||
|
Ok(Response::builder()
|
||||||
|
.header("Content-Type", "application/xml")
|
||||||
|
.body(string_body(xml))?)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn handle_list_buckets(
|
pub async fn handle_list_buckets(
|
||||||
garage: &Garage,
|
garage: &Garage,
|
||||||
api_key: &Key,
|
api_key: &Key,
|
||||||
@@ -311,6 +360,15 @@ fn parse_create_bucket_xml(xml_bytes: &[u8]) -> Option<Option<String>> {
|
|||||||
Some(ret)
|
Some(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_grantee(key_params: &KeyParams, api_key: &Key) -> s3_xml::Grantee {
|
||||||
|
s3_xml::Grantee {
|
||||||
|
xmlns_xsi: (),
|
||||||
|
typ: "CanonicalUser".to_string(),
|
||||||
|
display_name: Some(s3_xml::Value(key_params.name.get().to_string())),
|
||||||
|
id: Some(s3_xml::Value(api_key.key_id.to_string())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
+49
-28
@@ -26,7 +26,7 @@ use garage_api_common::signature::checksum::*;
|
|||||||
use crate::api_server::{ReqBody, ResBody};
|
use crate::api_server::{ReqBody, ResBody};
|
||||||
use crate::encryption::EncryptionParams;
|
use crate::encryption::EncryptionParams;
|
||||||
use crate::error::*;
|
use crate::error::*;
|
||||||
use crate::get::{full_object_byte_stream, PreconditionHeaders};
|
use crate::get::{check_version_not_deleted, full_object_byte_stream, PreconditionHeaders};
|
||||||
use crate::multipart;
|
use crate::multipart;
|
||||||
use crate::put::{extract_metadata_headers, save_stream, ChecksumMode, SaveStreamResult};
|
use crate::put::{extract_metadata_headers, save_stream, ChecksumMode, SaveStreamResult};
|
||||||
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
|
use crate::website::X_AMZ_WEBSITE_REDIRECT_LOCATION;
|
||||||
@@ -237,6 +237,7 @@ async fn handle_copy_metaonly(
|
|||||||
.get(&source_version.uuid, &EmptyKey)
|
.get(&source_version.uuid, &EmptyKey)
|
||||||
.await?;
|
.await?;
|
||||||
let source_version = source_version.ok_or(Error::NoSuchKey)?;
|
let source_version = source_version.ok_or(Error::NoSuchKey)?;
|
||||||
|
check_version_not_deleted(&source_version)?;
|
||||||
|
|
||||||
// Write an "uploading" marker in Object table
|
// Write an "uploading" marker in Object table
|
||||||
// This holds a reference to the object in the Version table
|
// This holds a reference to the object in the Version table
|
||||||
@@ -428,6 +429,7 @@ pub async fn handle_upload_part_copy(
|
|||||||
.get(&source_object_version.uuid, &EmptyKey)
|
.get(&source_object_version.uuid, &EmptyKey)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::NoSuchKey)?;
|
.ok_or(Error::NoSuchKey)?;
|
||||||
|
check_version_not_deleted(&source_version)?;
|
||||||
|
|
||||||
// We want to reuse blocks from the source version as much as possible.
|
// We want to reuse blocks from the source version as much as possible.
|
||||||
// However, we still need to get the data from these blocks
|
// However, we still need to get the data from these blocks
|
||||||
@@ -559,6 +561,7 @@ pub async fn handle_upload_part_copy(
|
|||||||
|
|
||||||
let mut current_offset = 0;
|
let mut current_offset = 0;
|
||||||
let mut next_block = defragmenter.next().await?;
|
let mut next_block = defragmenter.next().await?;
|
||||||
|
let mut blocks_to_dup = dest_version.clone();
|
||||||
|
|
||||||
// TODO this could be optimized similarly to read_and_put_blocks
|
// TODO this could be optimized similarly to read_and_put_blocks
|
||||||
// low priority because uploadpartcopy is rarely used
|
// low priority because uploadpartcopy is rarely used
|
||||||
@@ -588,8 +591,7 @@ pub async fn handle_upload_part_copy(
|
|||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
checksummer = checksummer_updated;
|
checksummer = checksummer_updated;
|
||||||
|
|
||||||
dest_version.blocks.clear();
|
let (version_block_key, version_block) = (
|
||||||
dest_version.blocks.put(
|
|
||||||
VersionBlockKey {
|
VersionBlockKey {
|
||||||
part_number,
|
part_number,
|
||||||
offset: current_offset,
|
offset: current_offset,
|
||||||
@@ -601,37 +603,56 @@ pub async fn handle_upload_part_copy(
|
|||||||
);
|
);
|
||||||
current_offset += data_len;
|
current_offset += data_len;
|
||||||
|
|
||||||
let block_ref = BlockRef {
|
let next = if let Some(final_data) = data_to_upload {
|
||||||
block: final_hash,
|
dest_version.blocks.clear();
|
||||||
version: dest_version_id,
|
dest_version.blocks.put(version_block_key, version_block);
|
||||||
deleted: false.into(),
|
let block_ref = BlockRef {
|
||||||
|
block: final_hash,
|
||||||
|
version: dest_version_id,
|
||||||
|
deleted: false.into(),
|
||||||
|
};
|
||||||
|
let (_, _, _, next) = futures::try_join!(
|
||||||
|
// Thing 1: if the block is not exactly a block that existed before,
|
||||||
|
// we need to insert that data as a new block.
|
||||||
|
garage.block_manager.rpc_put_block(
|
||||||
|
final_hash,
|
||||||
|
final_data,
|
||||||
|
dest_encryption.is_encrypted(),
|
||||||
|
None
|
||||||
|
),
|
||||||
|
// Thing 2: we need to insert the block in the version
|
||||||
|
garage.version_table.insert(&dest_version),
|
||||||
|
// Thing 3: we need to add a block reference
|
||||||
|
garage.block_ref_table.insert(&block_ref),
|
||||||
|
// Thing 4: we need to read the next block
|
||||||
|
defragmenter.next(),
|
||||||
|
)?;
|
||||||
|
next
|
||||||
|
} else {
|
||||||
|
blocks_to_dup.blocks.put(version_block_key, version_block);
|
||||||
|
defragmenter.next().await?
|
||||||
};
|
};
|
||||||
|
|
||||||
let (_, _, _, next) = futures::try_join!(
|
|
||||||
// Thing 1: if the block is not exactly a block that existed before,
|
|
||||||
// we need to insert that data as a new block.
|
|
||||||
async {
|
|
||||||
if let Some(final_data) = data_to_upload {
|
|
||||||
garage
|
|
||||||
.block_manager
|
|
||||||
.rpc_put_block(final_hash, final_data, dest_encryption.is_encrypted(), None)
|
|
||||||
.await
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Thing 2: we need to insert the block in the version
|
|
||||||
garage.version_table.insert(&dest_version),
|
|
||||||
// Thing 3: we need to add a block reference
|
|
||||||
garage.block_ref_table.insert(&block_ref),
|
|
||||||
// Thing 4: we need to read the next block
|
|
||||||
defragmenter.next(),
|
|
||||||
)?;
|
|
||||||
next_block = next;
|
next_block = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(current_offset, source_range.length);
|
assert_eq!(current_offset, source_range.length);
|
||||||
|
|
||||||
|
// Put the duplicated blocks into the version & block_refs tables
|
||||||
|
let block_refs_to_put = blocks_to_dup
|
||||||
|
.blocks
|
||||||
|
.items()
|
||||||
|
.iter()
|
||||||
|
.map(|b| BlockRef {
|
||||||
|
block: b.1.hash,
|
||||||
|
version: dest_version_id,
|
||||||
|
deleted: false.into(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
futures::try_join!(
|
||||||
|
garage.version_table.insert(&blocks_to_dup),
|
||||||
|
garage.block_ref_table.insert_many(&block_refs_to_put[..]),
|
||||||
|
)?;
|
||||||
|
|
||||||
let checksums = checksummer.finalize();
|
let checksums = checksummer.finalize();
|
||||||
let etag = dest_encryption.etag_from_md5(&checksums.md5);
|
let etag = dest_encryption.etag_from_md5(&checksums.md5);
|
||||||
let checksum = checksums.extract(dest_object_checksum_algorithm);
|
let checksum = checksums.extract(dest_object_checksum_algorithm);
|
||||||
|
|||||||
+30
-2
@@ -19,12 +19,13 @@ use garage_net::stream::ByteStream;
|
|||||||
use garage_rpc::rpc_helper::OrderTag;
|
use garage_rpc::rpc_helper::OrderTag;
|
||||||
use garage_table::EmptyKey;
|
use garage_table::EmptyKey;
|
||||||
use garage_util::data::*;
|
use garage_util::data::*;
|
||||||
use garage_util::error::OkOrMessage;
|
use garage_util::error::{Error as UtilError, OkOrMessage};
|
||||||
|
|
||||||
use garage_model::garage::Garage;
|
use garage_model::garage::Garage;
|
||||||
use garage_model::s3::object_table::*;
|
use garage_model::s3::object_table::*;
|
||||||
use garage_model::s3::version_table::*;
|
use garage_model::s3::version_table::*;
|
||||||
|
|
||||||
|
use garage_api_common::common_error::CommonError;
|
||||||
use garage_api_common::helpers::*;
|
use garage_api_common::helpers::*;
|
||||||
use garage_api_common::signature::checksum::{add_checksum_response_headers, X_AMZ_CHECKSUM_MODE};
|
use garage_api_common::signature::checksum::{add_checksum_response_headers, X_AMZ_CHECKSUM_MODE};
|
||||||
|
|
||||||
@@ -215,6 +216,7 @@ pub async fn handle_head_without_ctx(
|
|||||||
.get(&object_version.uuid, &EmptyKey)
|
.get(&object_version.uuid, &EmptyKey)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::NoSuchKey)?;
|
.ok_or(Error::NoSuchKey)?;
|
||||||
|
check_version_not_deleted(&version)?;
|
||||||
|
|
||||||
let (part_offset, part_end) =
|
let (part_offset, part_end) =
|
||||||
calculate_part_bounds(&version, pn).ok_or(Error::InvalidPart)?;
|
calculate_part_bounds(&version, pn).ok_or(Error::InvalidPart)?;
|
||||||
@@ -365,6 +367,21 @@ pub async fn handle_get_without_ctx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn check_version_not_deleted(version: &Version) -> Result<(), Error> {
|
||||||
|
if version.deleted.get() {
|
||||||
|
// the version was deleted between when the object_table was consulted
|
||||||
|
// and now, this could mean the object was deleted, or overriden.
|
||||||
|
// Rather than say the key doesn't exist, return a transient error
|
||||||
|
// to signal the client to try again.
|
||||||
|
return Err(CommonError::InternalError(UtilError::Message(
|
||||||
|
"conflict/inconsistency between object and version state, version is deleted"
|
||||||
|
.to_string(),
|
||||||
|
))
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_get_full(
|
async fn handle_get_full(
|
||||||
garage: Arc<Garage>,
|
garage: Arc<Garage>,
|
||||||
version: &ObjectVersion,
|
version: &ObjectVersion,
|
||||||
@@ -431,6 +448,7 @@ pub fn full_object_byte_stream(
|
|||||||
.ok_or_message("channel closed")?;
|
.ok_or_message("channel closed")?;
|
||||||
|
|
||||||
let version = version_fut.await.unwrap()?.ok_or(Error::NoSuchKey)?;
|
let version = version_fut.await.unwrap()?.ok_or(Error::NoSuchKey)?;
|
||||||
|
check_version_not_deleted(&version)?;
|
||||||
for (i, (_, vb)) in version.blocks.items().iter().enumerate().skip(1) {
|
for (i, (_, vb)) in version.blocks.items().iter().enumerate().skip(1) {
|
||||||
let stream_block_i = encryption
|
let stream_block_i = encryption
|
||||||
.get_block(&garage, &vb.hash, Some(order_stream.order(i as u64)))
|
.get_block(&garage, &vb.hash, Some(order_stream.order(i as u64)))
|
||||||
@@ -446,6 +464,14 @@ pub fn full_object_byte_stream(
|
|||||||
{
|
{
|
||||||
Ok(()) => (),
|
Ok(()) => (),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
// TODO i think this is a bad idea, we should log
|
||||||
|
// an error and stop there. If the error happens to
|
||||||
|
// be exactly the size of what hasn't been streamed
|
||||||
|
// yet, the client will see the request as a
|
||||||
|
// success
|
||||||
|
// instead truncating the output notify the client
|
||||||
|
// something happened with their download, so that
|
||||||
|
// they can retry it
|
||||||
let _ = tx.send(error_stream_item(e)).await;
|
let _ = tx.send(error_stream_item(e)).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -497,7 +523,7 @@ async fn handle_get_range(
|
|||||||
.get(&version.uuid, &EmptyKey)
|
.get(&version.uuid, &EmptyKey)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::NoSuchKey)?;
|
.ok_or(Error::NoSuchKey)?;
|
||||||
|
check_version_not_deleted(&version)?;
|
||||||
let body =
|
let body =
|
||||||
body_from_blocks_range(garage, encryption, version.blocks.items(), begin, end);
|
body_from_blocks_range(garage, encryption, version.blocks.items(), begin, end);
|
||||||
Ok(resp_builder.body(body)?)
|
Ok(resp_builder.body(body)?)
|
||||||
@@ -548,6 +574,8 @@ async fn handle_get_part(
|
|||||||
.await?
|
.await?
|
||||||
.ok_or(Error::NoSuchKey)?;
|
.ok_or(Error::NoSuchKey)?;
|
||||||
|
|
||||||
|
check_version_not_deleted(&version)?;
|
||||||
|
|
||||||
let (begin, end) =
|
let (begin, end) =
|
||||||
calculate_part_bounds(&version, part_number).ok_or(Error::InvalidPart)?;
|
calculate_part_bounds(&version, part_number).ok_or(Error::InvalidPart)?;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ pub fn xmlns_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
|
|||||||
s.serialize_str("http://s3.amazonaws.com/doc/2006-03-01/")
|
s.serialize_str("http://s3.amazonaws.com/doc/2006-03-01/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn xmlns_xsi_tag<S: Serializer>(_v: &(), s: S) -> Result<S::Ok, S::Error> {
|
||||||
|
s.serialize_str("http://www.w3.org/2001/XMLSchema-instance")
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct Value(#[serde(rename = "$value")] pub String);
|
pub struct Value(#[serde(rename = "$value")] pub String);
|
||||||
|
|
||||||
@@ -319,6 +323,42 @@ pub struct PostObject {
|
|||||||
pub etag: Value,
|
pub etag: Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct Grantee {
|
||||||
|
#[serde(rename = "xmlns:xsi", serialize_with = "xmlns_xsi_tag")]
|
||||||
|
pub xmlns_xsi: (),
|
||||||
|
#[serde(rename = "xsi:type")]
|
||||||
|
pub typ: String,
|
||||||
|
#[serde(rename = "DisplayName")]
|
||||||
|
pub display_name: Option<Value>,
|
||||||
|
#[serde(rename = "ID")]
|
||||||
|
pub id: Option<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct Grant {
|
||||||
|
#[serde(rename = "Grantee")]
|
||||||
|
pub grantee: Grantee,
|
||||||
|
#[serde(rename = "Permission")]
|
||||||
|
pub permission: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct AccessControlList {
|
||||||
|
#[serde(rename = "Grant")]
|
||||||
|
pub entries: Vec<Grant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct AccessControlPolicy {
|
||||||
|
#[serde(serialize_with = "xmlns_tag")]
|
||||||
|
pub xmlns: (),
|
||||||
|
#[serde(rename = "Owner")]
|
||||||
|
pub owner: Option<Owner>,
|
||||||
|
#[serde(rename = "AccessControlList")]
|
||||||
|
pub acl: AccessControlList,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -427,6 +467,43 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_bucket_acl_result() -> Result<(), ApiError> {
|
||||||
|
let grant = Grant {
|
||||||
|
grantee: Grantee {
|
||||||
|
xmlns_xsi: (),
|
||||||
|
typ: "CanonicalUser".to_string(),
|
||||||
|
display_name: Some(Value("owner_name".to_string())),
|
||||||
|
id: Some(Value("qsdfjklm".to_string())),
|
||||||
|
},
|
||||||
|
permission: Value("FULL_CONTROL".to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let get_bucket_acl = AccessControlPolicy {
|
||||||
|
xmlns: (),
|
||||||
|
owner: None,
|
||||||
|
acl: AccessControlList {
|
||||||
|
entries: vec![grant],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
to_xml_with_header(&get_bucket_acl)?,
|
||||||
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
|
||||||
|
<AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
|
||||||
|
<AccessControlList>\
|
||||||
|
<Grant>\
|
||||||
|
<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">\
|
||||||
|
<DisplayName>owner_name</DisplayName>\
|
||||||
|
<ID>qsdfjklm</ID>\
|
||||||
|
</Grantee>\
|
||||||
|
<Permission>FULL_CONTROL</Permission>\
|
||||||
|
</Grant>\
|
||||||
|
</AccessControlList>\
|
||||||
|
</AccessControlPolicy>"
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn delete_result() -> Result<(), ApiError> {
|
fn delete_result() -> Result<(), ApiError> {
|
||||||
let delete_result = DeleteResult {
|
let delete_result = DeleteResult {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_block"
|
name = "garage_block"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_db"
|
name = "garage_db"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage"
|
name = "garage"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
@@ -57,6 +57,7 @@ opentelemetry.workspace = true
|
|||||||
opentelemetry-prometheus = { workspace = true, optional = true }
|
opentelemetry-prometheus = { workspace = true, optional = true }
|
||||||
opentelemetry-otlp = { workspace = true, optional = true }
|
opentelemetry-otlp = { workspace = true, optional = true }
|
||||||
syslog-tracing = { workspace = true, optional = true }
|
syslog-tracing = { workspace = true, optional = true }
|
||||||
|
tracing-journald = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
garage_api_common.workspace = true
|
garage_api_common.workspace = true
|
||||||
@@ -101,6 +102,8 @@ metrics = [ "garage_api_admin/metrics", "opentelemetry-prometheus" ]
|
|||||||
telemetry-otlp = [ "opentelemetry-otlp" ]
|
telemetry-otlp = [ "opentelemetry-otlp" ]
|
||||||
# Logging to syslog
|
# Logging to syslog
|
||||||
syslog = [ "syslog-tracing" ]
|
syslog = [ "syslog-tracing" ]
|
||||||
|
# Logging to journald
|
||||||
|
journald = [ "tracing-journald" ]
|
||||||
|
|
||||||
# NOTE: bundled-libs and system-libs should be treat as mutually exclusive;
|
# NOTE: bundled-libs and system-libs should be treat as mutually exclusive;
|
||||||
# exactly one of them should be enabled.
|
# exactly one of them should be enabled.
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ impl AdminRpcHandler {
|
|||||||
let mut obj_dels = 0;
|
let mut obj_dels = 0;
|
||||||
let mut mpu_dels = 0;
|
let mut mpu_dels = 0;
|
||||||
let mut ver_dels = 0;
|
let mut ver_dels = 0;
|
||||||
|
let mut br_dels = 0;
|
||||||
|
|
||||||
for hash in blocks {
|
for hash in blocks {
|
||||||
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
let hash = hex::decode(hash).ok_or_bad_request("invalid hash")?;
|
||||||
@@ -131,12 +132,19 @@ impl AdminRpcHandler {
|
|||||||
ver_dels += 1;
|
ver_dels += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !br.deleted.get() {
|
||||||
|
let mut br = br;
|
||||||
|
br.deleted.set();
|
||||||
|
self.garage.block_ref_table.insert(&br).await?;
|
||||||
|
br_dels += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(AdminRpc::Ok(format!(
|
Ok(AdminRpc::Ok(format!(
|
||||||
"Purged {} blocks, {} versions, {} objects, {} multipart uploads",
|
"Purged {} blocks: marked {} block refs, {} versions, {} objects and {} multipart uploads as deleted",
|
||||||
blocks.len(),
|
blocks.len(),
|
||||||
|
br_dels,
|
||||||
ver_dels,
|
ver_dels,
|
||||||
obj_dels,
|
obj_dels,
|
||||||
mpu_dels,
|
mpu_dels,
|
||||||
|
|||||||
@@ -208,6 +208,43 @@ fn init_logging(opt: &Opt) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if std::env::var("GARAGE_LOG_TO_JOURNALD")
|
||||||
|
.map(|x| x == "1" || x == "true")
|
||||||
|
.unwrap_or(false)
|
||||||
|
{
|
||||||
|
#[cfg(feature = "journald")]
|
||||||
|
{
|
||||||
|
use tracing_journald::{Priority, PriorityMappings};
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
|
||||||
|
let registry = tracing_subscriber::registry()
|
||||||
|
.with(tracing_subscriber::fmt::layer().with_writer(std::io::sink))
|
||||||
|
.with(env_filter);
|
||||||
|
match tracing_journald::layer() {
|
||||||
|
Ok(layer) => {
|
||||||
|
registry
|
||||||
|
.with(layer.with_priority_mappings(PriorityMappings {
|
||||||
|
info: Priority::Informational,
|
||||||
|
debug: Priority::Debug,
|
||||||
|
..PriorityMappings::new()
|
||||||
|
}))
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Couldn't connect to journald: {}.", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "journald"))]
|
||||||
|
{
|
||||||
|
eprintln!("Journald support is not enabled in this build.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
tracing_subscriber::fmt()
|
||||||
.with_writer(std::io::stderr)
|
.with_writer(std::io::stderr)
|
||||||
.with_env_filter(env_filter)
|
.with_env_filter(env_filter)
|
||||||
|
|||||||
@@ -606,3 +606,45 @@ async fn test_website_puny() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_website_object_not_found() {
|
||||||
|
const BCKT_NAME: &str = "not-found";
|
||||||
|
let ctx = common::context();
|
||||||
|
let _bucket = ctx.create_bucket(BCKT_NAME);
|
||||||
|
|
||||||
|
let client = Client::builder(TokioExecutor::new()).build_http();
|
||||||
|
|
||||||
|
let req = |suffix| {
|
||||||
|
Request::builder()
|
||||||
|
.method("GET")
|
||||||
|
.uri(format!("http://127.0.0.1:{}/", ctx.garage.web_port))
|
||||||
|
.header("Host", format!("{}{}", BCKT_NAME, suffix))
|
||||||
|
.body(Body::new(Bytes::new()))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
ctx.garage
|
||||||
|
.command()
|
||||||
|
.args(["bucket", "website", "--allow", BCKT_NAME])
|
||||||
|
.quiet()
|
||||||
|
.expect_success_status("Could not allow website on bucket");
|
||||||
|
|
||||||
|
let resp = client.request(req("")).await.unwrap();
|
||||||
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
// the error we return by default are *not* xml
|
||||||
|
assert_eq!(
|
||||||
|
resp.headers().get(http::header::CONTENT_TYPE).unwrap(),
|
||||||
|
"text/html; charset=utf-8"
|
||||||
|
);
|
||||||
|
let result = String::from_utf8(
|
||||||
|
resp.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.to_vec(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(result.contains("not found"));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_model"
|
name = "garage_model"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_net"
|
name = "garage_net"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_rpc"
|
name = "garage_rpc"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_table"
|
name = "garage_table"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_util"
|
name = "garage_util"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>"]
|
authors = ["Alex Auvolat <alex@adnab.me>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "garage_web"
|
name = "garage_web"
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
authors = ["Alex Auvolat <alex@adnab.me>", "Quentin Dufour <quentin@dufour.io>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
|||||||
+22
-2
@@ -397,10 +397,30 @@ fn error_to_res(e: Error) -> Response<BoxBody<Error>> {
|
|||||||
// was a HEAD request or we couldn't get the error document)
|
// was a HEAD request or we couldn't get the error document)
|
||||||
// We do NOT enter this code path when returning the bucket's
|
// We do NOT enter this code path when returning the bucket's
|
||||||
// error document (this is handled in serve_file)
|
// error document (this is handled in serve_file)
|
||||||
let body = string_body(format!("{}\n", e));
|
let mut body_str = format!(
|
||||||
let mut http_error = Response::new(body);
|
r"<title>{http_code} {code_text}</title>
|
||||||
|
<h1>{http_code} {code_text}</h1>",
|
||||||
|
http_code = e.http_status_code().as_u16(),
|
||||||
|
code_text = e.http_status_code().canonical_reason().unwrap_or("Unknown"),
|
||||||
|
);
|
||||||
|
if let Error::ApiError(ref err) = e {
|
||||||
|
body_str.push_str(&format!(
|
||||||
|
r"
|
||||||
|
<ul>
|
||||||
|
<li>Code: {s3_code}</li>
|
||||||
|
<li>Message: {s3_message}.</li>
|
||||||
|
</ul>",
|
||||||
|
s3_code = err.aws_code(),
|
||||||
|
s3_message = err,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut http_error = Response::new(string_body(body_str));
|
||||||
*http_error.status_mut() = e.http_status_code();
|
*http_error.status_mut() = e.http_status_code();
|
||||||
e.add_headers(http_error.headers_mut());
|
e.add_headers(http_error.headers_mut());
|
||||||
|
http_error.headers_mut().insert(
|
||||||
|
http::header::CONTENT_TYPE,
|
||||||
|
"text/html; charset=utf-8".parse().unwrap(),
|
||||||
|
);
|
||||||
http_error
|
http_error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user