Bug fixing

This commit is contained in:
Your Name
2025-05-25 12:32:06 +12:00
parent 9308f4d719
commit 477d06d3bf
6 changed files with 261 additions and 207 deletions

193
README.md
View File

@@ -1,95 +1,138 @@
# Simple Object Storage
## Introduction
A simple object storage system that stores files with metadata and provides a REST API for access.
Simple Object Storage is a very simple C++ webserver
which provides a store of tagged binary objects (the objects can be large),
which are available over http.
## Features
Read access is public.
Write access is controlled by tokens.
- Store files with metadata (labels, tags, and custom fields)
- Retrieve files by hash or label:tag combination
- Check if a file exists by hash or label:tag
- Delete files by hash
- List all stored objects
- Automatic file deduplication using content hashing
- Support for large file uploads
- Configurable storage location and server settings
- Token-based authentication for write operations
Public read actions:
## Building
### Retrieve Object
- Objects are accessed via a label and tag, or via their hash. For example:
- `wget http://localhost:8123/object/squashkiwi:latest`
- `wget http://localhost:8123/object/4528400792837739857`
### Check Object Existence
- Check if an object exists by label:tag or hash:
- `curl http://localhost:8123/exists/squashkiwi:latest`
- `curl http://localhost:8123/exists/4528400792837739857`
### Retrieve Hash
- Get the hash for a given label and tag:
- `curl http://localhost:8123/hash/squashkiwi:latest`
- Response format: `{"result":"success","hash":"4528400792837739857"}`
### List Store Contents
- Get a full list of {label:tag,hash} entries:
- `curl http://localhost:8123/dir`
- Response format: `{"result":"success","entries":[{"label_tag":"example:latest","hash":"4528400792837739857"}]}`
### Retrieve Metadata
- Get all metadata for a tag:
- `curl http://localhost:8123/meta/squashkiwi:latest`
- `curl http://localhost:8123/meta/4528400792837739857`
- Response format: `{"result":"success","metadata":{"description":"Example file","tags":["test","example"],"custom_field":"custom value"}}`
### Service Status Check
- Quick status check:
- `curl http://localhost:8123/status`
- Response format: `{"result":"success","status":"ok"}`
## Write actions (require authentication):
### Upload Object
- Upload a file with metadata (via HTTP PUT):
```bash
curl -X PUT \
-H "Authorization: Bearer YOUR_TOKEN" \
-F "file=@/path/to/your/file.txt" \
-F 'metadata={"label":"example","tags":["latest","test","example"],"description":"Example file","custom_field":"custom value"}' \
"http://localhost:8123/upload"
mkdir build
cd build
cmake ..
make
```
- The object file is uploaded, hashed, added to the registry (if that hash doesn't already exist), and {label:tag,hash} entries are added to the directory index.
- Matching tags on older versions are removed.
- Response format: `{"result":"success","hash":"4528400792837739857"}`
### Delete Object
- Delete an object and all its tags:
- `curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8123/deleteobject?hash=4528400792837739857`
- Response format: `{"result":"success"}`
## Configuration
- The server is configured via `~/.config/simple_object_storage/config.json` which allows setting:
- `write_tokens`: List of valid write access tokens
- `object_store_path`: Location for the object store (path on disk)
- `host`: Server host (default: "0.0.0.0")
- `port`: Server port (default: 8123)
Example config.json:
The server can be configured by creating a JSON configuration file at `~/.config/simple_object_storage/config.json`. Here's an example configuration:
```json
{
"write_tokens": ["your-secret-token-1", "your-secret-token-2"],
"object_store_path": "/data/storage",
"host": "0.0.0.0",
"port": 8123
"host": "localhost",
"port": 8080,
"storage_path": "/path/to/storage",
"write_tokens": ["your-secret-token"]
}
```
## Signal Handling
## API Endpoints
The server handles the following signals:
### Upload a File
- `SIGTERM`/`SIGINT`: Gracefully shuts down the server when received (e.g. from Ctrl+C or system shutdown)
- `SIGHUP`: Reloads the server configuration without restarting the service
```
PUT /upload
```
The server ensures proper cleanup of resources during shutdown, including:
- Closing all database connections
- Stopping the HTTP server
- Cleaning up any open file handles
- Properly terminating worker threads
Parameters:
- `file`: The file to upload
- `metadata`: JSON object containing:
- `labels`: Array of strings (required)
- `tags`: Array of strings (required)
- Additional custom fields (optional)
Dockcross is used to cross-build for both 64-bit x86 and arm64 (combining both into one docker container image).
Example:
```bash
curl -X PUT \
-H "Authorization: Bearer your-token" \
-F "file=@example.txt" \
-F 'metadata={"labels":["test"],"tags":["latest"],"description":"Example file"}' \
http://localhost:8080/upload
```
### Get a File
```
GET /object/{hash}
GET /object/{label}:{tag}
```
Example:
```bash
curl http://localhost:8080/object/abc123
curl http://localhost:8080/object/test:latest
```
### Check if a File Exists
```
GET /exists/{hash}
GET /exists/{label}:{tag}
```
Example:
```bash
curl http://localhost:8080/exists/abc123
curl http://localhost:8080/exists/test:latest
```
### Delete a File
```
GET /deleteobject?hash={hash}
```
Example:
```bash
curl -H "Authorization: Bearer your-token" http://localhost:8080/deleteobject?hash=abc123
```
### List All Objects
```
GET /list
```
Example:
```bash
curl http://localhost:8080/list
```
## Database Schema
The system uses SQLite to store metadata about uploaded files. The database schema is as follows:
```sql
CREATE TABLE objects (
hash TEXT PRIMARY KEY,
labels TEXT NOT NULL, -- JSON array of labels
tags TEXT NOT NULL, -- JSON array of tags
metadata TEXT NOT NULL -- JSON object with additional metadata
);
```
## Testing
The repository includes two test scripts:
- `test.sh`: Basic functionality tests
- `test_1GB_file_upload.sh`: Tests uploading and downloading a 1GB file
To run the tests:
```bash
./test.sh
./test_1GB_file_upload.sh
```
## License
This project is licensed under the MIT License - see the LICENSE file for details.

View File

@@ -1,11 +1,31 @@
#include <stdexcept>
#include <sstream>
#include <set>
#include "database.hpp"
#include "sqlite3/sqlite3.h"
namespace simple_object_storage {
bool Database::createObjectsTable() {
const char* create_table_sql =
"CREATE TABLE IF NOT EXISTS objects ("
"hash TEXT PRIMARY KEY,"
"labels TEXT NOT NULL," // JSON array of labels
"tags TEXT NOT NULL," // JSON array of tags
"metadata TEXT NOT NULL"
");";
char* err_msg = nullptr;
int rc = sqlite3_exec(db_, create_table_sql, nullptr, nullptr, &err_msg);
if (rc != SQLITE_OK) {
std::string error = err_msg;
sqlite3_free(err_msg);
return false;
}
return true;
}
bool Database::createVersionTable() {
const char* sql =
"CREATE TABLE IF NOT EXISTS version_info ("
@@ -80,9 +100,21 @@ bool Database::setVersion(int version) {
}
bool Database::migrate(int from_version, int to_version) {
// Currently only one version, so no migrations needed
// This method will be expanded when we need to add new versions
return true;
if (from_version == 1 && to_version == 2) {
// Drop old table
const char* drop_sql = "DROP TABLE IF EXISTS objects;";
char* err_msg = nullptr;
int rc = sqlite3_exec(db_, drop_sql, nullptr, nullptr, &err_msg);
if (rc != SQLITE_OK) {
std::string error = err_msg;
sqlite3_free(err_msg);
return false;
}
// Create new table with updated schema
return createObjectsTable();
}
return false;
}
Database::Database(const std::filesystem::path& path) : path_(path) {
@@ -112,19 +144,8 @@ Database::Database(const std::filesystem::path& path) : path_(path) {
}
// Create objects table if it doesn't exist
const char* create_table_sql =
"CREATE TABLE IF NOT EXISTS objects ("
"label_tag TEXT PRIMARY KEY,"
"hash TEXT NOT NULL,"
"metadata TEXT NOT NULL"
");";
char* err_msg = nullptr;
rc = sqlite3_exec(db_, create_table_sql, nullptr, nullptr, &err_msg);
if (rc != SQLITE_OK) {
std::string error = err_msg;
sqlite3_free(err_msg);
throw std::runtime_error("Failed to create table: " + error);
if (!createObjectsTable()) {
throw std::runtime_error("Failed to create objects table");
}
}
@@ -134,24 +155,6 @@ Database::~Database() {
}
}
bool Database::insert(const dbEntry& entry) {
std::string sql = "INSERT INTO objects (label_tag, hash, metadata) VALUES (?, ?, ?);";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
sqlite3_bind_text(stmt, 1, entry.label_tag.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, entry.hash.c_str(), -1, SQLITE_STATIC);
std::string metadata_str = entry.metadata.dump();
sqlite3_bind_text(stmt, 3, metadata_str.c_str(), -1, SQLITE_STATIC);
bool success = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return success;
}
bool Database::remove(const std::string& label_tag) {
std::string sql = "DELETE FROM objects WHERE label_tag = ?;";
sqlite3_stmt* stmt;
@@ -180,50 +183,36 @@ bool Database::remove_by_hash(const std::string& hash) {
return success;
}
bool Database::get(const std::string& label_tag, dbEntry& entry) {
std::string sql = "SELECT hash, metadata FROM objects WHERE label_tag = ?;";
bool Database::get(const std::string& hash, dbEntry& entry) {
std::string sql = "SELECT labels, tags, metadata FROM objects WHERE hash = ?;";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
sqlite3_bind_text(stmt, 1, label_tag.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 1, hash.c_str(), -1, SQLITE_STATIC);
if (sqlite3_step(stmt) != SQLITE_ROW) {
sqlite3_finalize(stmt);
return false;
}
entry.label_tag = label_tag;
entry.hash = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
std::string metadata_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
entry.hash = hash;
std::string labels_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
std::string tags_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
std::string metadata_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
entry.labels = nlohmann::json::parse(labels_str).get<std::vector<std::string>>();
entry.tags = nlohmann::json::parse(tags_str).get<std::vector<std::string>>();
entry.metadata = nlohmann::json::parse(metadata_str);
sqlite3_finalize(stmt);
return true;
}
bool Database::update(const std::string& label_tag, const dbEntry& entry) {
std::string sql = "UPDATE objects SET hash = ?, metadata = ? WHERE label_tag = ?;";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
sqlite3_bind_text(stmt, 1, entry.hash.c_str(), -1, SQLITE_STATIC);
std::string metadata_str = entry.metadata.dump();
sqlite3_bind_text(stmt, 2, metadata_str.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, label_tag.c_str(), -1, SQLITE_STATIC);
bool success = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return success;
}
bool Database::list(std::vector<dbEntry>& entries) {
std::string sql = "SELECT label_tag, hash, metadata FROM objects;";
std::string sql = "SELECT hash, labels, tags, metadata FROM objects;";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
@@ -233,9 +222,13 @@ bool Database::list(std::vector<dbEntry>& entries) {
entries.clear();
while (sqlite3_step(stmt) == SQLITE_ROW) {
dbEntry entry;
entry.label_tag = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
entry.hash = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
std::string metadata_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
entry.hash = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
std::string labels_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 1));
std::string tags_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 2));
std::string metadata_str = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 3));
entry.labels = nlohmann::json::parse(labels_str).get<std::vector<std::string>>();
entry.tags = nlohmann::json::parse(tags_str).get<std::vector<std::string>>();
entry.metadata = nlohmann::json::parse(metadata_str);
entries.push_back(entry);
}
@@ -245,21 +238,69 @@ bool Database::list(std::vector<dbEntry>& entries) {
}
bool Database::update_or_insert(const dbEntry& entry) {
std::string sql = "INSERT OR REPLACE INTO objects (label_tag, hash, metadata) VALUES (?, ?, ?);";
sqlite3_stmt* stmt;
// First try to get existing entry
dbEntry existing;
bool exists = get(entry.hash, existing);
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
return false;
if (exists) {
// Merge labels and tags
std::set<std::string> merged_labels(existing.labels.begin(), existing.labels.end());
merged_labels.insert(entry.labels.begin(), entry.labels.end());
std::set<std::string> merged_tags(existing.tags.begin(), existing.tags.end());
merged_tags.insert(entry.tags.begin(), entry.tags.end());
// Create new entry with merged data
dbEntry merged = entry;
merged.labels = std::vector<std::string>(merged_labels.begin(), merged_labels.end());
merged.tags = std::vector<std::string>(merged_tags.begin(), merged_tags.end());
// Merge metadata
for (const auto& [key, value] : entry.metadata.items()) {
merged.metadata[key] = value;
}
// Update database
std::string sql = "UPDATE objects SET labels = ?, tags = ?, metadata = ? WHERE hash = ?;";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
std::string labels_str = nlohmann::json(merged.labels).dump();
std::string tags_str = nlohmann::json(merged.tags).dump();
std::string metadata_str = merged.metadata.dump();
sqlite3_bind_text(stmt, 1, labels_str.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, tags_str.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, metadata_str.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 4, merged.hash.c_str(), -1, SQLITE_STATIC);
bool success = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return success;
} else {
// Insert new entry
std::string sql = "INSERT INTO objects (hash, labels, tags, metadata) VALUES (?, ?, ?, ?);";
sqlite3_stmt* stmt;
if (sqlite3_prepare_v2(db_, sql.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
return false;
}
std::string labels_str = nlohmann::json(entry.labels).dump();
std::string tags_str = nlohmann::json(entry.tags).dump();
std::string metadata_str = entry.metadata.dump();
sqlite3_bind_text(stmt, 1, entry.hash.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, labels_str.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, tags_str.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 4, metadata_str.c_str(), -1, SQLITE_STATIC);
bool success = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return success;
}
sqlite3_bind_text(stmt, 1, entry.label_tag.c_str(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, entry.hash.c_str(), -1, SQLITE_STATIC);
std::string metadata_str = entry.metadata.dump();
sqlite3_bind_text(stmt, 3, metadata_str.c_str(), -1, SQLITE_STATIC);
bool success = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return success;
}
} // namespace simple_object_storage

View File

@@ -11,22 +11,21 @@ namespace simple_object_storage {
class dbEntry {
public:
std::string label_tag; // unique identifier for the object
std::string hash; // hash of the object - not unique
std::string hash; // unique primary key
std::vector<std::string> labels; // multiple labels
std::vector<std::string> tags; // multiple tags
nlohmann::json metadata;
};
class Database {
public:
static const int CURRENT_VERSION = 1;
static const int CURRENT_VERSION = 2;
Database(const std::filesystem::path& path);
~Database();
bool insert(const dbEntry& entry);
bool remove(const std::string& label_tag);
bool remove(const std::string& hash);
bool remove_by_hash(const std::string& hash);
bool get(const std::string& label_tag, dbEntry& entry);
bool update(const std::string& label_tag, const dbEntry& entry);
bool get(const std::string& hash, dbEntry& entry);
bool list(std::vector<dbEntry>& entries);
bool update_or_insert(const dbEntry& entry);
private:
@@ -37,6 +36,7 @@ class Database {
bool getVersion(int& version);
bool setVersion(int version);
bool migrate(int from_version, int to_version);
bool createObjectsTable();
};
} // namespace simple_object_storage

View File

@@ -67,9 +67,9 @@ void PutHandler::handle_put_object(const httplib::Request& req, httplib::Respons
}
// Validate required metadata fields
if (!metadata.contains("label")) {
if (!metadata.contains("labels") || !metadata["labels"].is_array() || metadata["labels"].empty()) {
res.status = 400;
nlohmann::json response = {{"result", "error"}, {"error", "Missing required metadata field: label"}};
nlohmann::json response = {{"result", "error"}, {"error", "Missing or invalid required metadata field: labels (must be non-empty array)"}};
res.set_content(response.dump(), "application/json");
return;
}
@@ -81,15 +81,6 @@ void PutHandler::handle_put_object(const httplib::Request& req, httplib::Respons
return;
}
// Extract label and tags
std::string label = metadata["label"];
if (label.empty()) {
res.status = 400;
nlohmann::json response = {{"result", "error"}, {"error", "Label cannot be empty"}};
res.set_content(response.dump(), "application/json");
return;
}
// Add filename to metadata if not provided
if (!metadata.contains("filename")) {
metadata["filename"] = file.filename;
@@ -147,6 +138,7 @@ void PutHandler::handle_put_object(const httplib::Request& req, httplib::Respons
// Move file to final location
std::filesystem::path final_path = server_.config_.object_store_path / std::to_string(hash);
if (!std::filesystem::exists(final_path)) {
try {
std::filesystem::rename(temp_path, final_path);
@@ -163,22 +155,11 @@ void PutHandler::handle_put_object(const httplib::Request& req, httplib::Respons
// Update database index
dbEntry entry;
entry.hash = std::to_string(hash);
entry.metadata = metadata; // Store the complete metadata
entry.labels = metadata["labels"].get<std::vector<std::string>>();
entry.tags = metadata["tags"].get<std::vector<std::string>>();
entry.metadata = metadata;
// For each tag, create a label:tag entry
bool success = true;
for (const auto& tag : metadata["tags"]) {
std::string tag_str = tag.get<std::string>();
if (tag_str.empty()) continue; // Skip empty tags
entry.label_tag = label + ":" + tag_str;
if (!server_.db_->update_or_insert(entry)) {
success = false;
break;
}
}
if (!success) {
if (!server_.db_->update_or_insert(entry)) {
res.status = 500;
nlohmann::json response = {{"result", "error"}, {"error", "Failed to update database index"}};
res.set_content(response.dump(), "application/json");

35
test.sh
View File

@@ -54,9 +54,9 @@ BASE_TAG="autotest"
# Construct metadata JSON
METADATA_JSON=$(cat <<EOF
{
"labeltag": "${BASE_TAG}:test1",
"labels": ["${BASE_TAG}"],
"tags": ["test1"],
"description": "Example file",
"tags": ["test", "example"],
"custom_field": "custom value"
}
EOF
@@ -74,10 +74,6 @@ echo "upload response: ${UPLOAD_RESPONSE}"
OBJECT_HASH=$(echo ${UPLOAD_RESPONSE} | jq -r '.hash')
#OBJECT_HASH=$(curl -s "${BASE_URL}/upload?token=${WRITE_TOKEN}&labeltag=${BASE_TAG}:test1&filename=${SCRIPT_NAME}" -T ${SCRIPT_DIR}/${SCRIPT_NAME} | jq -r '.hash')
echo "received hash ${OBJECT_HASH}"
# check the hash matches.
CHECK_HASH=$(curl -s "${BASE_URL}/hash/${BASE_TAG}:test1" | jq -r '.hash')
[ "${OBJECT_HASH}" != "${CHECK_HASH}" ] && die "hash does not match: ${OBJECT_HASH} != ${CHECK_HASH}"
@@ -108,7 +104,6 @@ MD5SUM_DOWNLOADED2=$(md5sum ${SCRIPT_DIR}/${SCRIPT_NAME}.downloaded2 | awk '{pri
rm ${SCRIPT_DIR}/${SCRIPT_NAME}.downloaded1
rm ${SCRIPT_DIR}/${SCRIPT_NAME}.downloaded2
# delete the object
echo "deleting ${OBJECT_HASH}"
if ! curl -s -H "Authorization: Bearer ${WRITE_TOKEN}" "${BASE_URL}/deleteobject?hash=${OBJECT_HASH}" | jq -r '.result' | grep -q 'success'; then
@@ -128,9 +123,9 @@ title "Testing metadata field preservation"
# Upload with extra metadata fields
EXTRA_METADATA_JSON=$(cat <<EOF
{
"labeltag": "${BASE_TAG}:test2",
"labels": ["${BASE_TAG}"],
"tags": ["test2"],
"description": "Test with extra fields",
"tags": ["test", "extra"],
"custom_field": "custom value",
"extra_field1": "value1",
"extra_field2": "value2"
@@ -195,9 +190,9 @@ title "Testing tag versioning behavior"
# Upload first version with tag 'latest'
FIRST_METADATA_JSON=$(cat <<EOF
{
"labeltag": "${BASE_TAG}:latest",
"description": "First version",
"tags": ["test", "v1"]
"labels": ["${BASE_TAG}"],
"tags": ["latest", "v1"],
"description": "First version"
}
EOF
)
@@ -221,9 +216,9 @@ fi
# Upload second version with same tag 'latest'
SECOND_METADATA_JSON=$(cat <<EOF
{
"labeltag": "${BASE_TAG}:latest",
"description": "Second version",
"tags": ["test", "v2"]
"labels": ["${BASE_TAG}"],
"tags": ["latest", "v2"],
"description": "Second version"
}
EOF
)
@@ -248,15 +243,12 @@ if ! echo "${FIRST_METADATA}" | jq -r '.metadata.tags[]' | grep -q 'v1'; then
die "First version does not have v1 tag"
fi
# Verify first version's metadata no longer has the latest or test tags.
# Verify first version's metadata no longer has the latest tag
if echo "${FIRST_METADATA}" | jq -r '.metadata.tags[]' | grep -q 'latest'; then
die "First version still has latest tag"
fi
if echo "${FIRST_METADATA}" | jq -r '.metadata.tags[]' | grep -q 'test'; then
die "First version still has test tag"
fi
# Verify second version has the correct tags: v2, latest, test!
# Verify second version has the correct tags: v2 and latest
SECOND_METADATA=$(curl -s "${BASE_URL}/meta/${BASE_TAG}:latest")
echo "Second version metadata response: ${SECOND_METADATA}"
if ! echo "${SECOND_METADATA}" | jq -r '.metadata.tags[]' | grep -q 'v2'; then
@@ -265,9 +257,6 @@ fi
if ! echo "${SECOND_METADATA}" | jq -r '.metadata.tags[]' | grep -q 'latest'; then
die "Second version does not have latest tag"
fi
if ! echo "${SECOND_METADATA}" | jq -r '.metadata.tags[]' | grep -q 'test'; then
die "Second version does not have test tag"
fi
# Clean up
curl -s -H "Authorization: Bearer ${WRITE_TOKEN}" "${BASE_URL}/deleteobject?hash=${FIRST_HASH}" > /dev/null

View File

@@ -29,7 +29,7 @@ echo "Uploading file..."
RESPONSE=$(curl -X PUT \
-H "Authorization: Bearer ${WRITE_TOKEN}" \
-F "file=@test_file.bin" \
-F 'metadata={"labeltag":"test:latest","description":"Test file","tags":["test","large"]}' \
-F 'metadata={"labels":["test"],"tags":["latest","large"],"description":"Test file"}' \
"http://${HOST}:${PORT}/upload")
echo "Upload response: $RESPONSE"