libzarr
Header-only C++17 Zarr v2/v3, WASM-compatible
Loading...
Searching...
No Matches
filesystem_store.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2
3#ifndef LIBZARR_ADAPTERS_FILESYSTEM_STORE_HPP
4#define LIBZARR_ADAPTERS_FILESYSTEM_STORE_HPP
5
6#include <algorithm>
7#include <cstdint>
8#include <filesystem>
9#include <fstream>
10#include <ios>
11#include <optional>
12#include <string>
13#include <string_view>
14#include <system_error>
15#include <utility>
16#include <vector>
17
18#include "libzarr/detail/common.hpp"
19#include "libzarr/store.hpp"
20#include "libzarr/types.hpp"
21
26
27namespace zarr {
28
30class FilesystemStore final : public Store {
31 public:
33 explicit FilesystemStore(std::filesystem::path root, bool create = true)
34 : root_(std::move(root)) {
35 if (create) {
36 std::filesystem::create_directories(root_);
37 }
38 }
39
40 [[nodiscard]] std::optional<Bytes> read(std::string_view key) override {
41 std::ifstream in(key_path(key), std::ios::binary);
42 if (!in) {
43 return std::nullopt;
44 }
45 Bytes out;
46 in.seekg(0, std::ios::end);
47 const std::streamoff size = in.tellg();
48 in.seekg(0, std::ios::beg);
49 out.resize(static_cast<std::size_t>(size));
50 in.read(reinterpret_cast<char*>(out.data()), size);
51 if (!in) {
52 throw error("failed to read '" + std::string(key) + "'");
53 }
54 return out;
55 }
56
57 [[nodiscard]] std::optional<Bytes> read_range(std::string_view key, ByteRange range) override {
58 if (range.kind == ByteRange::Kind::full) {
59 return read(key);
60 }
61 std::ifstream in(key_path(key), std::ios::binary);
62 if (!in) {
63 return std::nullopt;
64 }
65 in.seekg(0, std::ios::end);
66 const auto size = static_cast<std::uint64_t>(in.tellg());
67 std::uint64_t begin = 0;
68 if (range.kind == ByteRange::Kind::slice) {
69 if (range.length > size || range.offset > size - range.length) {
70 throw error("read_range: slice at offset " + std::to_string(range.offset) + " of length " +
71 std::to_string(range.length) + " out of bounds for '" + std::string(key) +
72 "' (" + std::to_string(size) + " bytes)");
73 }
74 begin = range.offset;
75 } else { // suffix
76 if (range.length > size) {
77 throw error("read_range: suffix of length " + std::to_string(range.length) +
78 " out of bounds for '" + std::string(key) + "' (" + std::to_string(size) +
79 " bytes)");
80 }
81 begin = size - range.length;
82 }
83 Bytes out(detail::checked_size(range.length, "read_range"));
84 in.seekg(static_cast<std::streamoff>(begin), std::ios::beg);
85 in.read(reinterpret_cast<char*>(out.data()), static_cast<std::streamsize>(out.size()));
86 if (!in) {
87 throw error("failed to read range from '" + std::string(key) + "'");
88 }
89 return out;
90 }
91
92 void write(std::string_view key, Bytes value) override {
93 const std::filesystem::path path = key_path(key);
94 std::filesystem::create_directories(path.parent_path());
95 std::ofstream out(path, std::ios::binary | std::ios::trunc);
96 if (!out) {
97 throw error("cannot open '" + std::string(key) + "' for writing");
98 }
99 out.write(reinterpret_cast<const char*>(value.data()),
100 static_cast<std::streamsize>(value.size()));
101 if (!out) {
102 throw error("failed to write '" + std::string(key) + "'");
103 }
104 }
105
106 [[nodiscard]] std::optional<std::uint64_t> size(std::string_view key) override {
107 std::error_code ec;
108 const auto bytes = std::filesystem::file_size(key_path(key), ec);
109 if (ec) {
110 return std::nullopt;
111 }
112 return bytes;
113 }
114
115 [[nodiscard]] bool exists(std::string_view key) override {
116 return std::filesystem::is_regular_file(key_path(key));
117 }
118
119 void erase(std::string_view key) override {
120 std::error_code ec;
121 std::filesystem::remove(key_path(key), ec); // absent key is a no-op
122 }
123
124 [[nodiscard]] std::vector<std::string> list_prefix(std::string_view prefix) override {
125 check_prefix(prefix);
126 std::vector<std::string> out;
127 if (!std::filesystem::is_directory(root_)) {
128 return out;
129 }
130 for (const auto& entry : std::filesystem::recursive_directory_iterator(root_)) {
131 if (!entry.is_regular_file()) {
132 continue;
133 }
134 const std::string key = std::filesystem::relative(entry.path(), root_).generic_string();
135 if (detail::starts_with(key, prefix)) {
136 out.push_back(key);
137 }
138 }
139 std::sort(out.begin(), out.end());
140 return out;
141 }
142
143 [[nodiscard]] DirListing list_dir(std::string_view prefix) override {
144 check_prefix(prefix);
145 DirListing out;
146 std::filesystem::path dir = root_;
147 if (!prefix.empty()) {
148 dir /= std::filesystem::path(prefix.substr(0, prefix.size() - 1));
149 }
150 if (!std::filesystem::is_directory(dir)) {
151 return out;
152 }
153 for (const auto& entry : std::filesystem::directory_iterator(dir)) {
154 const std::string name = entry.path().filename().generic_string();
155 if (entry.is_regular_file()) {
156 out.keys.push_back(name);
157 } else if (entry.is_directory()) {
158 out.prefixes.push_back(name);
159 }
160 }
161 std::sort(out.keys.begin(), out.keys.end());
162 std::sort(out.prefixes.begin(), out.prefixes.end());
163 return out;
164 }
165
166 private:
167 static void check_prefix(std::string_view prefix) {
168 if (!prefix.empty() && prefix.back() != '/') {
169 throw error("store prefix must be empty or end with '/', got '" + std::string(prefix) + "'");
170 }
171 }
172
173 [[nodiscard]] std::filesystem::path key_path(std::string_view key) const {
174 if (key.empty()) {
175 throw error("store key must not be empty");
176 }
177 std::filesystem::path path = root_;
178 std::size_t start = 0;
179 while (start <= key.size()) {
180 const std::size_t slash = key.find('/', start);
181 const std::size_t end = slash == std::string_view::npos ? key.size() : slash;
182 const std::string_view segment = key.substr(start, end - start);
183 // Reject anything that could escape the root directory.
184 if (segment.empty() || segment == "." || segment == "..") {
185 throw error("invalid store key '" + std::string(key) + "'");
186 }
187 path /= segment;
188 if (slash == std::string_view::npos) {
189 break;
190 }
191 start = slash + 1;
192 }
193 return path;
194 }
195
196 std::filesystem::path root_;
197};
198
199} // namespace zarr
200
201#endif // LIBZARR_ADAPTERS_FILESYSTEM_STORE_HPP
Store mapping keys to files under a root directory.
Definition filesystem_store.hpp:30
bool exists(std::string_view key) override
True if key holds a value.
Definition filesystem_store.hpp:115
std::optional< Bytes > read(std::string_view key) override
Full value at key, or std::nullopt if the key is absent.
Definition filesystem_store.hpp:40
std::optional< Bytes > read_range(std::string_view key, ByteRange range) override
Definition filesystem_store.hpp:57
std::optional< std::uint64_t > size(std::string_view key) override
Definition filesystem_store.hpp:106
DirListing list_dir(std::string_view prefix) override
Immediate children under prefix ("" or ending in '/').
Definition filesystem_store.hpp:143
FilesystemStore(std::filesystem::path root, bool create=true)
Binds to root, creating the directory when create is true.
Definition filesystem_store.hpp:33
void erase(std::string_view key) override
Remove key; removing an absent key is a no-op.
Definition filesystem_store.hpp:119
void write(std::string_view key, Bytes value) override
Create or replace the value at key.
Definition filesystem_store.hpp:92
std::vector< std::string > list_prefix(std::string_view prefix) override
All keys starting with prefix ("" or ending in '/'), sorted.
Definition filesystem_store.hpp:124
Definition store.hpp:83
Definition types.hpp:36
Byte-range request for Store::read_range.
Definition store.hpp:28
@ slice
length bytes starting at offset
@ full
the whole value
std::uint64_t offset
Start of the range; used by Kind::slice only.
Definition store.hpp:39
std::uint64_t length
Number of bytes; used by Kind::slice and Kind::suffix.
Definition store.hpp:41
Kind kind
Which part of the value to read.
Definition store.hpp:37
Immediate children of a prefix, as returned by Store::list_dir.
Definition store.hpp:70
std::vector< std::string > keys
Child keys, relative to the queried prefix, sorted.
Definition store.hpp:72
std::vector< std::string > prefixes
Child prefixes ("directories"), relative, without trailing '/', sorted.
Definition store.hpp:74
std::vector< std::uint8_t > Bytes
Owned byte buffer used throughout the value-based public API.
Definition types.hpp:42