Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions cpp/src/arrow/filesystem/gcsfs.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ namespace {

namespace gcs = google::cloud::storage;

auto constexpr kSep = '/';
// Change the default upload buffer size. In general, sending larger buffers is more
// efficient with GCS, as each buffer requires a roundtrip to the service. With formatted
// output (when using `operator<<`), keeping a larger buffer in memory before uploading
Expand All@@ -49,18 +48,17 @@ struct GcsPath {
std::string object;

static Result<GcsPath> FromString(const std::string& s) {
const auto src = internal::RemoveTrailingSlash(s);
auto const first_sep = src.find_first_of(kSep);
auto const first_sep = s.find_first_of(internal::kSep);
if (first_sep == 0) {
return Status::Invalid("Path cannot start with a separator ('", s, "')");
}
if (first_sep == std::string::npos) {
return GcsPath{std::string(src), std::string(src), ""};
return GcsPath{s, internal::RemoveTrailingSlash(s).to_string(), ""};
}
GcsPath path;
path.full_path = std::string(src);
path.bucket = std::string(src.substr(0, first_sep));
path.object = std::string(src.substr(first_sep + 1));
path.full_path = s;
path.bucket = s.substr(0, first_sep);
path.object = s.substr(first_sep + 1);
return path;
}

Expand DownExpand Up@@ -275,12 +273,73 @@ class GcsFileSystem::Impl {
const GcsOptions& options() const { return options_; }

Result<FileInfo> GetFileInfo(const GcsPath& path) {
if (!path.object.empty()) {
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::File);
if (path.object.empty()) {
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
}
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(
path, std::move(meta).status(),
path.object.back() == '/' ? FileType::Directory : FileType::File);
}

// GCS does not have directories or folders. But folders can be emulated (with some
// limitations) using marker objects. That and listing with prefixes creates the
// illusion of folders.
google::cloud::Status CreateDirMarker(const std::string& bucket,
util::string_view name) {
// Make the name canonical.
const auto canonical = internal::EnsureTrailingSlash(name);
return client_
.InsertObject(bucket, canonical, std::string(),
gcs::WithObjectMetadata(gcs::ObjectMetadata().upsert_metadata(
"arrow/gcsfs", "directory")))
.status();
}

google::cloud::Status CreateDirMarkerRecursive(const std::string& bucket,
const std::string& object) {
using GcsCode = google::cloud::StatusCode;
auto get_parent = [](std::string const& path) {
return std::move(internal::GetAbstractPathParent(path).first);
};
// Maybe counterintuitively we create the markers from the most nested and up. Because
// GCS does not have directories creating `a/b/c` will succeed, even if `a/` or `a/b/`
// does not exist. In the common case, where `a/b/` may already exist, it is more
// efficient to just create `a/b/c/` and then find out that `a/b/` was already there.
// In the case where none exists, it does not matter which order we follow.
auto status = CreateDirMarker(bucket, object);
if (status.code() == GcsCode::kAlreadyExists) return {};
if (status.code() == GcsCode::kNotFound) {
// Missing bucket, create it first ...
status = client_.CreateBucket(bucket, gcs::BucketMetadata()).status();
if (status.code() != GcsCode::kOk && status.code() != GcsCode::kAlreadyExists) {
return status;
}
}

for (auto parent = get_parent(object); !parent.empty(); parent = get_parent(parent)) {
status = CreateDirMarker(bucket, parent);
if (status.code() == GcsCode::kAlreadyExists) {
break;
}
if (!status.ok()) {
return status;
}
}
return {};
}

Status CreateDir(const GcsPath& p) {
if (p.object.empty()) {
return internal::ToArrowStatus(
client_.CreateBucket(p.bucket, gcs::BucketMetadata()).status());
}
return internal::ToArrowStatus(CreateDirMarker(p.bucket, p.object));
}

Status CreateDirRecursive(const GcsPath& p) {
return internal::ToArrowStatus(CreateDirMarkerRecursive(p.bucket, p.object));
}

Status DeleteFile(const GcsPath& p) {
Expand DownExpand Up@@ -332,12 +391,15 @@ class GcsFileSystem::Impl {
static Result<FileInfo> GetFileInfoImpl(const GcsPath& path,
const google::cloud::Status& status,
FileType type) {
const auto& canonical = type == FileType::Directory
? internal::EnsureTrailingSlash(path.full_path)
: path.full_path;
Comment thread
pitrou marked this conversation as resolved.
if (status.ok()) {
return FileInfo(path.full_path, type);
return FileInfo(canonical, type);
}
using ::google::cloud::StatusCode;
if (status.code() == StatusCode::kNotFound) {
return FileInfo(path.full_path, FileType::NotFound);
return FileInfo(canonical, FileType::NotFound);
}
return internal::ToArrowStatus(status);
}
Expand DownExpand Up@@ -373,7 +435,9 @@ Result<FileInfoVector> GcsFileSystem::GetFileInfo(const FileSelector& select) {
}

Status GcsFileSystem::CreateDir(const std::string& path, bool recursive) {
return Status::NotImplemented("The GCS FileSystem is not fully implemented");
ARROW_ASSIGN_OR_RAISE(auto p, GcsPath::FromString(path));
if (!recursive) return impl_->CreateDir(p);
return impl_->CreateDirRecursive(p);
}

Status GcsFileSystem::DeleteDir(const std::string& path) {
Expand Down
52 changes: 36 additions & 16 deletions cpp/src/arrow/filesystem/gcsfs.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,24 +40,43 @@ struct ARROW_EXPORT GcsOptions {
bool Equals(const GcsOptions& other) const;
};

// - TODO(ARROW-1231) - review this documentation before closing the bug.
/// \brief GCS-backed FileSystem implementation.
///
/// Some implementation notes:
/// - TODO(ARROW-1231) - review all the notes once completed.
/// - buckets are treated as top-level directories on a "root".
/// - GCS buckets are in a global namespace, only one bucket
/// named `foo` exists in Google Cloud.
/// - Creating new top-level directories is implemented by creating
/// a bucket, this may be a slower operation than usual.
/// - A principal (service account, user, etc) can only list the
/// buckets for a single project, but can access the buckets
/// for many projects. It is possible that listing "all"
/// the buckets returns fewer buckets than you have access to.
/// - GCS does not have directories, they are emulated in this
/// library by listing objects with a common prefix.
/// - In general, GCS has much higher latency than local filesystems.
/// The throughput of GCS is comparable to the throughput of
/// a local file system.
/// GCS (Google Cloud Storage - https://cloud.google.com/storage) is a scalable object
/// storage system for any amount of data. The main abstractions in GCS are buckets and
/// objects. A bucket is a namespace for objects, buckets can store any number of objects,
/// tens of millions and even billions is not uncommon. Each object contains a single
/// blob of data, up to 5TiB in size. Buckets are typically configured to keep a single
/// version of each object, but versioning can be enabled. Versioning is important because
/// objects are immutable, once created one cannot append data to the object or modify the
/// object data in any way.
///
/// GCS buckets are in a global namespace, if a Google Cloud customer creates a bucket
/// named `foo` no other customer can create a bucket with the same name. Note that a
/// principal (a user or service account) may only list the buckets they are entitled to,
/// and then only within a project. It is not possible to list "all" the buckets.
///
/// Within each bucket objects are in flat namespace. GCS does not have folders or
/// directories. However, following some conventions it is possible to emulate
/// directories. To this end, this class:
///
/// - All buckets are treated as directories at the "root"
/// - Creating a root directory results in a new bucket being created, this may be slower
/// than most GCS operations.
/// - Any object with a name ending with a slash (`/`) character is treated as a
/// directory.
/// - The class creates marker objects for a directory, using a trailing slash in the
/// marker names. For debugging purposes, the metadata of these marker objects indicate
/// that they are markers created by this class. The class does not rely on this
/// annotation.
/// - GCS can list all the objects with a given prefix, this is used to emulate listing
/// of directories.
/// - In object lists GCS can summarize all the objects with a common prefix as a single
/// entry, this is used to emulate non-recursive lists. Note that GCS list time is
/// proportional to the number of objects in the prefix. Listing recursively takes
/// almost the same time as non-recursive lists.
///
class ARROW_EXPORT GcsFileSystem : public FileSystem {
public:
~GcsFileSystem() override = default;
Expand All@@ -75,6 +94,7 @@ class ARROW_EXPORT GcsFileSystem : public FileSystem {

Status DeleteDirContents(const std::string& path) override;

/// This is not implemented in GcsFileSystem, as it would be too dangerous.
Status DeleteRootDirContents() override;

Status DeleteFile(const std::string& path) override;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
ARROW-14917: [C++] Implement GcsFileSystem::CreateDir by coryan · Pull Request #11842 · apache/arrow · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions cpp/src/arrow/filesystem/gcsfs.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ namespace {

namespace gcs = google::cloud::storage;

auto constexpr kSep = '/';
// Change the default upload buffer size. In general, sending larger buffers is more
// efficient with GCS, as each buffer requires a roundtrip to the service. With formatted
// output (when using `operator<<`), keeping a larger buffer in memory before uploading
Expand All@@ -49,18 +48,17 @@ struct GcsPath {
std::string object;

static Result<GcsPath> FromString(const std::string& s) {
const auto src = internal::RemoveTrailingSlash(s);
auto const first_sep = src.find_first_of(kSep);
auto const first_sep = s.find_first_of(internal::kSep);
if (first_sep == 0) {
return Status::Invalid("Path cannot start with a separator ('", s, "')");
}
if (first_sep == std::string::npos) {
return GcsPath{std::string(src), std::string(src), ""};
return GcsPath{s, internal::RemoveTrailingSlash(s).to_string(), ""};
}
GcsPath path;
path.full_path = std::string(src);
path.bucket = std::string(src.substr(0, first_sep));
path.object = std::string(src.substr(first_sep + 1));
path.full_path = s;
path.bucket = s.substr(0, first_sep);
path.object = s.substr(first_sep + 1);
return path;
}

Expand DownExpand Up@@ -275,12 +273,73 @@ class GcsFileSystem::Impl {
const GcsOptions& options() const { return options_; }

Result<FileInfo> GetFileInfo(const GcsPath& path) {
if (!path.object.empty()) {
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::File);
if (path.object.empty()) {
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
}
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(
path, std::move(meta).status(),
path.object.back() == '/' ? FileType::Directory : FileType::File);
}

// GCS does not have directories or folders. But folders can be emulated (with some
// limitations) using marker objects. That and listing with prefixes creates the
// illusion of folders.
google::cloud::Status CreateDirMarker(const std::string& bucket,
util::string_view name) {
// Make the name canonical.
const auto canonical = internal::EnsureTrailingSlash(name);
return client_
.InsertObject(bucket, canonical, std::string(),
gcs::WithObjectMetadata(gcs::ObjectMetadata().upsert_metadata(
"arrow/gcsfs", "directory")))
.status();
}

google::cloud::Status CreateDirMarkerRecursive(const std::string& bucket,
const std::string& object) {
using GcsCode = google::cloud::StatusCode;
auto get_parent = [](std::string const& path) {
return std::move(internal::GetAbstractPathParent(path).first);
};
// Maybe counterintuitively we create the markers from the most nested and up. Because
// GCS does not have directories creating `a/b/c` will succeed, even if `a/` or `a/b/`
// does not exist. In the common case, where `a/b/` may already exist, it is more
// efficient to just create `a/b/c/` and then find out that `a/b/` was already there.
// In the case where none exists, it does not matter which order we follow.
auto status = CreateDirMarker(bucket, object);
if (status.code() == GcsCode::kAlreadyExists) return {};
if (status.code() == GcsCode::kNotFound) {
// Missing bucket, create it first ...
status = client_.CreateBucket(bucket, gcs::BucketMetadata()).status();
if (status.code() != GcsCode::kOk && status.code() != GcsCode::kAlreadyExists) {
return status;
}
}

for (auto parent = get_parent(object); !parent.empty(); parent = get_parent(parent)) {
status = CreateDirMarker(bucket, parent);
if (status.code() == GcsCode::kAlreadyExists) {
break;
}
if (!status.ok()) {
return status;
}
}
return {};
}

Status CreateDir(const GcsPath& p) {
if (p.object.empty()) {
return internal::ToArrowStatus(
client_.CreateBucket(p.bucket, gcs::BucketMetadata()).status());
}
return internal::ToArrowStatus(CreateDirMarker(p.bucket, p.object));
}

Status CreateDirRecursive(const GcsPath& p) {
return internal::ToArrowStatus(CreateDirMarkerRecursive(p.bucket, p.object));
}

Status DeleteFile(const GcsPath& p) {
Expand DownExpand Up@@ -332,12 +391,15 @@ class GcsFileSystem::Impl {
static Result<FileInfo> GetFileInfoImpl(const GcsPath& path,
const google::cloud::Status& status,
FileType type) {
const auto& canonical = type == FileType::Directory
? internal::EnsureTrailingSlash(path.full_path)
: path.full_path;
Comment thread
pitrou marked this conversation as resolved.
if (status.ok()) {
return FileInfo(path.full_path, type);
return FileInfo(canonical, type);
}
using ::google::cloud::StatusCode;
if (status.code() == StatusCode::kNotFound) {
return FileInfo(path.full_path, FileType::NotFound);
return FileInfo(canonical, FileType::NotFound);
}
return internal::ToArrowStatus(status);
}
Expand DownExpand Up@@ -373,7 +435,9 @@ Result<FileInfoVector> GcsFileSystem::GetFileInfo(const FileSelector& select) {
}

Status GcsFileSystem::CreateDir(const std::string& path, bool recursive) {
return Status::NotImplemented("The GCS FileSystem is not fully implemented");
ARROW_ASSIGN_OR_RAISE(auto p, GcsPath::FromString(path));
if (!recursive) return impl_->CreateDir(p);
return impl_->CreateDirRecursive(p);
}

Status GcsFileSystem::DeleteDir(const std::string& path) {
Expand Down
52 changes: 36 additions & 16 deletions cpp/src/arrow/filesystem/gcsfs.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,24 +40,43 @@ struct ARROW_EXPORT GcsOptions {
bool Equals(const GcsOptions& other) const;
};

// - TODO(ARROW-1231) - review this documentation before closing the bug.
/// \brief GCS-backed FileSystem implementation.
///
/// Some implementation notes:
/// - TODO(ARROW-1231) - review all the notes once completed.
/// - buckets are treated as top-level directories on a "root".
/// - GCS buckets are in a global namespace, only one bucket
/// named `foo` exists in Google Cloud.
/// - Creating new top-level directories is implemented by creating
/// a bucket, this may be a slower operation than usual.
/// - A principal (service account, user, etc) can only list the
/// buckets for a single project, but can access the buckets
/// for many projects. It is possible that listing "all"
/// the buckets returns fewer buckets than you have access to.
/// - GCS does not have directories, they are emulated in this
/// library by listing objects with a common prefix.
/// - In general, GCS has much higher latency than local filesystems.
/// The throughput of GCS is comparable to the throughput of
/// a local file system.
/// GCS (Google Cloud Storage - https://cloud.google.com/storage) is a scalable object
/// storage system for any amount of data. The main abstractions in GCS are buckets and
/// objects. A bucket is a namespace for objects, buckets can store any number of objects,
/// tens of millions and even billions is not uncommon. Each object contains a single
/// blob of data, up to 5TiB in size. Buckets are typically configured to keep a single
/// version of each object, but versioning can be enabled. Versioning is important because
/// objects are immutable, once created one cannot append data to the object or modify the
/// object data in any way.
///
/// GCS buckets are in a global namespace, if a Google Cloud customer creates a bucket
/// named `foo` no other customer can create a bucket with the same name. Note that a
/// principal (a user or service account) may only list the buckets they are entitled to,
/// and then only within a project. It is not possible to list "all" the buckets.
///
/// Within each bucket objects are in flat namespace. GCS does not have folders or
/// directories. However, following some conventions it is possible to emulate
/// directories. To this end, this class:
///
/// - All buckets are treated as directories at the "root"
/// - Creating a root directory results in a new bucket being created, this may be slower
/// than most GCS operations.
/// - Any object with a name ending with a slash (`/`) character is treated as a
/// directory.
/// - The class creates marker objects for a directory, using a trailing slash in the
/// marker names. For debugging purposes, the metadata of these marker objects indicate
/// that they are markers created by this class. The class does not rely on this
/// annotation.
/// - GCS can list all the objects with a given prefix, this is used to emulate listing
/// of directories.
/// - In object lists GCS can summarize all the objects with a common prefix as a single
/// entry, this is used to emulate non-recursive lists. Note that GCS list time is
/// proportional to the number of objects in the prefix. Listing recursively takes
/// almost the same time as non-recursive lists.
///
class ARROW_EXPORT GcsFileSystem : public FileSystem {
public:
~GcsFileSystem() override = default;
Expand All@@ -75,6 +94,7 @@ class ARROW_EXPORT GcsFileSystem : public FileSystem {

Status DeleteDirContents(const std::string& path) override;

/// This is not implemented in GcsFileSystem, as it would be too dangerous.
Status DeleteRootDirContents() override;

Status DeleteFile(const std::string& path) override;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ARROW-14917: [C++] Implement GcsFileSystem::CreateDir by coryan · Pull Request #11842 · apache/arrow · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions cpp/src/arrow/filesystem/gcsfs.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ namespace {

namespace gcs = google::cloud::storage;

auto constexpr kSep = '/';
// Change the default upload buffer size. In general, sending larger buffers is more
// efficient with GCS, as each buffer requires a roundtrip to the service. With formatted
// output (when using `operator<<`), keeping a larger buffer in memory before uploading
Expand All@@ -49,18 +48,17 @@ struct GcsPath {
std::string object;

static Result<GcsPath> FromString(const std::string& s) {
const auto src = internal::RemoveTrailingSlash(s);
auto const first_sep = src.find_first_of(kSep);
auto const first_sep = s.find_first_of(internal::kSep);
if (first_sep == 0) {
return Status::Invalid("Path cannot start with a separator ('", s, "')");
}
if (first_sep == std::string::npos) {
return GcsPath{std::string(src), std::string(src), ""};
return GcsPath{s, internal::RemoveTrailingSlash(s).to_string(), ""};
}
GcsPath path;
path.full_path = std::string(src);
path.bucket = std::string(src.substr(0, first_sep));
path.object = std::string(src.substr(first_sep + 1));
path.full_path = s;
path.bucket = s.substr(0, first_sep);
path.object = s.substr(first_sep + 1);
return path;
}

Expand DownExpand Up@@ -275,12 +273,73 @@ class GcsFileSystem::Impl {
const GcsOptions& options() const { return options_; }

Result<FileInfo> GetFileInfo(const GcsPath& path) {
if (!path.object.empty()) {
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::File);
if (path.object.empty()) {
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
}
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(
path, std::move(meta).status(),
path.object.back() == '/' ? FileType::Directory : FileType::File);
}

// GCS does not have directories or folders. But folders can be emulated (with some
// limitations) using marker objects. That and listing with prefixes creates the
// illusion of folders.
google::cloud::Status CreateDirMarker(const std::string& bucket,
util::string_view name) {
// Make the name canonical.
const auto canonical = internal::EnsureTrailingSlash(name);
return client_
.InsertObject(bucket, canonical, std::string(),
gcs::WithObjectMetadata(gcs::ObjectMetadata().upsert_metadata(
"arrow/gcsfs", "directory")))
.status();
}

google::cloud::Status CreateDirMarkerRecursive(const std::string& bucket,
const std::string& object) {
using GcsCode = google::cloud::StatusCode;
auto get_parent = [](std::string const& path) {
return std::move(internal::GetAbstractPathParent(path).first);
};
// Maybe counterintuitively we create the markers from the most nested and up. Because
// GCS does not have directories creating `a/b/c` will succeed, even if `a/` or `a/b/`
// does not exist. In the common case, where `a/b/` may already exist, it is more
// efficient to just create `a/b/c/` and then find out that `a/b/` was already there.
// In the case where none exists, it does not matter which order we follow.
auto status = CreateDirMarker(bucket, object);
if (status.code() == GcsCode::kAlreadyExists) return {};
if (status.code() == GcsCode::kNotFound) {
// Missing bucket, create it first ...
status = client_.CreateBucket(bucket, gcs::BucketMetadata()).status();
if (status.code() != GcsCode::kOk && status.code() != GcsCode::kAlreadyExists) {
return status;
}
}

for (auto parent = get_parent(object); !parent.empty(); parent = get_parent(parent)) {
status = CreateDirMarker(bucket, parent);
if (status.code() == GcsCode::kAlreadyExists) {
break;
}
if (!status.ok()) {
return status;
}
}
return {};
}

Status CreateDir(const GcsPath& p) {
if (p.object.empty()) {
return internal::ToArrowStatus(
client_.CreateBucket(p.bucket, gcs::BucketMetadata()).status());
}
return internal::ToArrowStatus(CreateDirMarker(p.bucket, p.object));
}

Status CreateDirRecursive(const GcsPath& p) {
return internal::ToArrowStatus(CreateDirMarkerRecursive(p.bucket, p.object));
}

Status DeleteFile(const GcsPath& p) {
Expand DownExpand Up@@ -332,12 +391,15 @@ class GcsFileSystem::Impl {
static Result<FileInfo> GetFileInfoImpl(const GcsPath& path,
const google::cloud::Status& status,
FileType type) {
const auto& canonical = type == FileType::Directory
? internal::EnsureTrailingSlash(path.full_path)
: path.full_path;
Comment thread
pitrou marked this conversation as resolved.
if (status.ok()) {
return FileInfo(path.full_path, type);
return FileInfo(canonical, type);
}
using ::google::cloud::StatusCode;
if (status.code() == StatusCode::kNotFound) {
return FileInfo(path.full_path, FileType::NotFound);
return FileInfo(canonical, FileType::NotFound);
}
return internal::ToArrowStatus(status);
}
Expand DownExpand Up@@ -373,7 +435,9 @@ Result<FileInfoVector> GcsFileSystem::GetFileInfo(const FileSelector& select) {
}

Status GcsFileSystem::CreateDir(const std::string& path, bool recursive) {
return Status::NotImplemented("The GCS FileSystem is not fully implemented");
ARROW_ASSIGN_OR_RAISE(auto p, GcsPath::FromString(path));
if (!recursive) return impl_->CreateDir(p);
return impl_->CreateDirRecursive(p);
}

Status GcsFileSystem::DeleteDir(const std::string& path) {
Expand Down
52 changes: 36 additions & 16 deletions cpp/src/arrow/filesystem/gcsfs.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,24 +40,43 @@ struct ARROW_EXPORT GcsOptions {
bool Equals(const GcsOptions& other) const;
};

// - TODO(ARROW-1231) - review this documentation before closing the bug.
/// \brief GCS-backed FileSystem implementation.
///
/// Some implementation notes:
/// - TODO(ARROW-1231) - review all the notes once completed.
/// - buckets are treated as top-level directories on a "root".
/// - GCS buckets are in a global namespace, only one bucket
/// named `foo` exists in Google Cloud.
/// - Creating new top-level directories is implemented by creating
/// a bucket, this may be a slower operation than usual.
/// - A principal (service account, user, etc) can only list the
/// buckets for a single project, but can access the buckets
/// for many projects. It is possible that listing "all"
/// the buckets returns fewer buckets than you have access to.
/// - GCS does not have directories, they are emulated in this
/// library by listing objects with a common prefix.
/// - In general, GCS has much higher latency than local filesystems.
/// The throughput of GCS is comparable to the throughput of
/// a local file system.
/// GCS (Google Cloud Storage - https://cloud.google.com/storage) is a scalable object
/// storage system for any amount of data. The main abstractions in GCS are buckets and
/// objects. A bucket is a namespace for objects, buckets can store any number of objects,
/// tens of millions and even billions is not uncommon. Each object contains a single
/// blob of data, up to 5TiB in size. Buckets are typically configured to keep a single
/// version of each object, but versioning can be enabled. Versioning is important because
/// objects are immutable, once created one cannot append data to the object or modify the
/// object data in any way.
///
/// GCS buckets are in a global namespace, if a Google Cloud customer creates a bucket
/// named `foo` no other customer can create a bucket with the same name. Note that a
/// principal (a user or service account) may only list the buckets they are entitled to,
/// and then only within a project. It is not possible to list "all" the buckets.
///
/// Within each bucket objects are in flat namespace. GCS does not have folders or
/// directories. However, following some conventions it is possible to emulate
/// directories. To this end, this class:
///
/// - All buckets are treated as directories at the "root"
/// - Creating a root directory results in a new bucket being created, this may be slower
/// than most GCS operations.
/// - Any object with a name ending with a slash (`/`) character is treated as a
/// directory.
/// - The class creates marker objects for a directory, using a trailing slash in the
/// marker names. For debugging purposes, the metadata of these marker objects indicate
/// that they are markers created by this class. The class does not rely on this
/// annotation.
/// - GCS can list all the objects with a given prefix, this is used to emulate listing
/// of directories.
/// - In object lists GCS can summarize all the objects with a common prefix as a single
/// entry, this is used to emulate non-recursive lists. Note that GCS list time is
/// proportional to the number of objects in the prefix. Listing recursively takes
/// almost the same time as non-recursive lists.
///
class ARROW_EXPORT GcsFileSystem : public FileSystem {
public:
~GcsFileSystem() override = default;
Expand All@@ -75,6 +94,7 @@ class ARROW_EXPORT GcsFileSystem : public FileSystem {

Status DeleteDirContents(const std::string& path) override;

/// This is not implemented in GcsFileSystem, as it would be too dangerous.
Status DeleteRootDirContents() override;

Status DeleteFile(const std::string& path) override;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ARROW-14917: [C++] Implement GcsFileSystem::CreateDir by coryan · Pull Request #11842 · apache/arrow · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions cpp/src/arrow/filesystem/gcsfs.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ namespace {

namespace gcs = google::cloud::storage;

auto constexpr kSep = '/';
// Change the default upload buffer size. In general, sending larger buffers is more
// efficient with GCS, as each buffer requires a roundtrip to the service. With formatted
// output (when using `operator<<`), keeping a larger buffer in memory before uploading
Expand All@@ -49,18 +48,17 @@ struct GcsPath {
std::string object;

static Result<GcsPath> FromString(const std::string& s) {
const auto src = internal::RemoveTrailingSlash(s);
auto const first_sep = src.find_first_of(kSep);
auto const first_sep = s.find_first_of(internal::kSep);
if (first_sep == 0) {
return Status::Invalid("Path cannot start with a separator ('", s, "')");
}
if (first_sep == std::string::npos) {
return GcsPath{std::string(src), std::string(src), ""};
return GcsPath{s, internal::RemoveTrailingSlash(s).to_string(), ""};
}
GcsPath path;
path.full_path = std::string(src);
path.bucket = std::string(src.substr(0, first_sep));
path.object = std::string(src.substr(first_sep + 1));
path.full_path = s;
path.bucket = s.substr(0, first_sep);
path.object = s.substr(first_sep + 1);
return path;
}

Expand DownExpand Up@@ -275,12 +273,73 @@ class GcsFileSystem::Impl {
const GcsOptions& options() const { return options_; }

Result<FileInfo> GetFileInfo(const GcsPath& path) {
if (!path.object.empty()) {
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::File);
if (path.object.empty()) {
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
}
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(
path, std::move(meta).status(),
path.object.back() == '/' ? FileType::Directory : FileType::File);
}

// GCS does not have directories or folders. But folders can be emulated (with some
// limitations) using marker objects. That and listing with prefixes creates the
// illusion of folders.
google::cloud::Status CreateDirMarker(const std::string& bucket,
util::string_view name) {
// Make the name canonical.
const auto canonical = internal::EnsureTrailingSlash(name);
return client_
.InsertObject(bucket, canonical, std::string(),
gcs::WithObjectMetadata(gcs::ObjectMetadata().upsert_metadata(
"arrow/gcsfs", "directory")))
.status();
}

google::cloud::Status CreateDirMarkerRecursive(const std::string& bucket,
const std::string& object) {
using GcsCode = google::cloud::StatusCode;
auto get_parent = [](std::string const& path) {
return std::move(internal::GetAbstractPathParent(path).first);
};
// Maybe counterintuitively we create the markers from the most nested and up. Because
// GCS does not have directories creating `a/b/c` will succeed, even if `a/` or `a/b/`
// does not exist. In the common case, where `a/b/` may already exist, it is more
// efficient to just create `a/b/c/` and then find out that `a/b/` was already there.
// In the case where none exists, it does not matter which order we follow.
auto status = CreateDirMarker(bucket, object);
if (status.code() == GcsCode::kAlreadyExists) return {};
if (status.code() == GcsCode::kNotFound) {
// Missing bucket, create it first ...
status = client_.CreateBucket(bucket, gcs::BucketMetadata()).status();
if (status.code() != GcsCode::kOk && status.code() != GcsCode::kAlreadyExists) {
return status;
}
}

for (auto parent = get_parent(object); !parent.empty(); parent = get_parent(parent)) {
status = CreateDirMarker(bucket, parent);
if (status.code() == GcsCode::kAlreadyExists) {
break;
}
if (!status.ok()) {
return status;
}
}
return {};
}

Status CreateDir(const GcsPath& p) {
if (p.object.empty()) {
return internal::ToArrowStatus(
client_.CreateBucket(p.bucket, gcs::BucketMetadata()).status());
}
return internal::ToArrowStatus(CreateDirMarker(p.bucket, p.object));
}

Status CreateDirRecursive(const GcsPath& p) {
return internal::ToArrowStatus(CreateDirMarkerRecursive(p.bucket, p.object));
}

Status DeleteFile(const GcsPath& p) {
Expand DownExpand Up@@ -332,12 +391,15 @@ class GcsFileSystem::Impl {
static Result<FileInfo> GetFileInfoImpl(const GcsPath& path,
const google::cloud::Status& status,
FileType type) {
const auto& canonical = type == FileType::Directory
? internal::EnsureTrailingSlash(path.full_path)
: path.full_path;
Comment thread
pitrou marked this conversation as resolved.
if (status.ok()) {
return FileInfo(path.full_path, type);
return FileInfo(canonical, type);
}
using ::google::cloud::StatusCode;
if (status.code() == StatusCode::kNotFound) {
return FileInfo(path.full_path, FileType::NotFound);
return FileInfo(canonical, FileType::NotFound);
}
return internal::ToArrowStatus(status);
}
Expand DownExpand Up@@ -373,7 +435,9 @@ Result<FileInfoVector> GcsFileSystem::GetFileInfo(const FileSelector& select) {
}

Status GcsFileSystem::CreateDir(const std::string& path, bool recursive) {
return Status::NotImplemented("The GCS FileSystem is not fully implemented");
ARROW_ASSIGN_OR_RAISE(auto p, GcsPath::FromString(path));
if (!recursive) return impl_->CreateDir(p);
return impl_->CreateDirRecursive(p);
}

Status GcsFileSystem::DeleteDir(const std::string& path) {
Expand Down
52 changes: 36 additions & 16 deletions cpp/src/arrow/filesystem/gcsfs.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,24 +40,43 @@ struct ARROW_EXPORT GcsOptions {
bool Equals(const GcsOptions& other) const;
};

// - TODO(ARROW-1231) - review this documentation before closing the bug.
/// \brief GCS-backed FileSystem implementation.
///
/// Some implementation notes:
/// - TODO(ARROW-1231) - review all the notes once completed.
/// - buckets are treated as top-level directories on a "root".
/// - GCS buckets are in a global namespace, only one bucket
/// named `foo` exists in Google Cloud.
/// - Creating new top-level directories is implemented by creating
/// a bucket, this may be a slower operation than usual.
/// - A principal (service account, user, etc) can only list the
/// buckets for a single project, but can access the buckets
/// for many projects. It is possible that listing "all"
/// the buckets returns fewer buckets than you have access to.
/// - GCS does not have directories, they are emulated in this
/// library by listing objects with a common prefix.
/// - In general, GCS has much higher latency than local filesystems.
/// The throughput of GCS is comparable to the throughput of
/// a local file system.
/// GCS (Google Cloud Storage - https://cloud.google.com/storage) is a scalable object
/// storage system for any amount of data. The main abstractions in GCS are buckets and
/// objects. A bucket is a namespace for objects, buckets can store any number of objects,
/// tens of millions and even billions is not uncommon. Each object contains a single
/// blob of data, up to 5TiB in size. Buckets are typically configured to keep a single
/// version of each object, but versioning can be enabled. Versioning is important because
/// objects are immutable, once created one cannot append data to the object or modify the
/// object data in any way.
///
/// GCS buckets are in a global namespace, if a Google Cloud customer creates a bucket
/// named `foo` no other customer can create a bucket with the same name. Note that a
/// principal (a user or service account) may only list the buckets they are entitled to,
/// and then only within a project. It is not possible to list "all" the buckets.
///
/// Within each bucket objects are in flat namespace. GCS does not have folders or
/// directories. However, following some conventions it is possible to emulate
/// directories. To this end, this class:
///
/// - All buckets are treated as directories at the "root"
/// - Creating a root directory results in a new bucket being created, this may be slower
/// than most GCS operations.
/// - Any object with a name ending with a slash (`/`) character is treated as a
/// directory.
/// - The class creates marker objects for a directory, using a trailing slash in the
/// marker names. For debugging purposes, the metadata of these marker objects indicate
/// that they are markers created by this class. The class does not rely on this
/// annotation.
/// - GCS can list all the objects with a given prefix, this is used to emulate listing
/// of directories.
/// - In object lists GCS can summarize all the objects with a common prefix as a single
/// entry, this is used to emulate non-recursive lists. Note that GCS list time is
/// proportional to the number of objects in the prefix. Listing recursively takes
/// almost the same time as non-recursive lists.
///
class ARROW_EXPORT GcsFileSystem : public FileSystem {
public:
~GcsFileSystem() override = default;
Expand All@@ -75,6 +94,7 @@ class ARROW_EXPORT GcsFileSystem : public FileSystem {

Status DeleteDirContents(const std::string& path) override;

/// This is not implemented in GcsFileSystem, as it would be too dangerous.
Status DeleteRootDirContents() override;

Status DeleteFile(const std::string& path) override;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' ARROW-14917: [C++] Implement GcsFileSystem::CreateDir by coryan · Pull Request #11842 · apache/arrow · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions cpp/src/arrow/filesystem/gcsfs.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ namespace {

namespace gcs = google::cloud::storage;

auto constexpr kSep = '/';
// Change the default upload buffer size. In general, sending larger buffers is more
// efficient with GCS, as each buffer requires a roundtrip to the service. With formatted
// output (when using `operator<<`), keeping a larger buffer in memory before uploading
Expand All@@ -49,18 +48,17 @@ struct GcsPath {
std::string object;

static Result<GcsPath> FromString(const std::string& s) {
const auto src = internal::RemoveTrailingSlash(s);
auto const first_sep = src.find_first_of(kSep);
auto const first_sep = s.find_first_of(internal::kSep);
if (first_sep == 0) {
return Status::Invalid("Path cannot start with a separator ('", s, "')");
}
if (first_sep == std::string::npos) {
return GcsPath{std::string(src), std::string(src), ""};
return GcsPath{s, internal::RemoveTrailingSlash(s).to_string(), ""};
}
GcsPath path;
path.full_path = std::string(src);
path.bucket = std::string(src.substr(0, first_sep));
path.object = std::string(src.substr(first_sep + 1));
path.full_path = s;
path.bucket = s.substr(0, first_sep);
path.object = s.substr(first_sep + 1);
return path;
}

Expand DownExpand Up@@ -275,12 +273,73 @@ class GcsFileSystem::Impl {
const GcsOptions& options() const { return options_; }

Result<FileInfo> GetFileInfo(const GcsPath& path) {
if (!path.object.empty()) {
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::File);
if (path.object.empty()) {
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
}
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(
path, std::move(meta).status(),
path.object.back() == '/' ? FileType::Directory : FileType::File);
}

// GCS does not have directories or folders. But folders can be emulated (with some
// limitations) using marker objects. That and listing with prefixes creates the
// illusion of folders.
google::cloud::Status CreateDirMarker(const std::string& bucket,
util::string_view name) {
// Make the name canonical.
const auto canonical = internal::EnsureTrailingSlash(name);
return client_
.InsertObject(bucket, canonical, std::string(),
gcs::WithObjectMetadata(gcs::ObjectMetadata().upsert_metadata(
"arrow/gcsfs", "directory")))
.status();
}

google::cloud::Status CreateDirMarkerRecursive(const std::string& bucket,
const std::string& object) {
using GcsCode = google::cloud::StatusCode;
auto get_parent = [](std::string const& path) {
return std::move(internal::GetAbstractPathParent(path).first);
};
// Maybe counterintuitively we create the markers from the most nested and up. Because
// GCS does not have directories creating `a/b/c` will succeed, even if `a/` or `a/b/`
// does not exist. In the common case, where `a/b/` may already exist, it is more
// efficient to just create `a/b/c/` and then find out that `a/b/` was already there.
// In the case where none exists, it does not matter which order we follow.
auto status = CreateDirMarker(bucket, object);
if (status.code() == GcsCode::kAlreadyExists) return {};
if (status.code() == GcsCode::kNotFound) {
// Missing bucket, create it first ...
status = client_.CreateBucket(bucket, gcs::BucketMetadata()).status();
if (status.code() != GcsCode::kOk && status.code() != GcsCode::kAlreadyExists) {
return status;
}
}

for (auto parent = get_parent(object); !parent.empty(); parent = get_parent(parent)) {
status = CreateDirMarker(bucket, parent);
if (status.code() == GcsCode::kAlreadyExists) {
break;
}
if (!status.ok()) {
return status;
}
}
return {};
}

Status CreateDir(const GcsPath& p) {
if (p.object.empty()) {
return internal::ToArrowStatus(
client_.CreateBucket(p.bucket, gcs::BucketMetadata()).status());
}
return internal::ToArrowStatus(CreateDirMarker(p.bucket, p.object));
}

Status CreateDirRecursive(const GcsPath& p) {
return internal::ToArrowStatus(CreateDirMarkerRecursive(p.bucket, p.object));
}

Status DeleteFile(const GcsPath& p) {
Expand DownExpand Up@@ -332,12 +391,15 @@ class GcsFileSystem::Impl {
static Result<FileInfo> GetFileInfoImpl(const GcsPath& path,
const google::cloud::Status& status,
FileType type) {
const auto& canonical = type == FileType::Directory
? internal::EnsureTrailingSlash(path.full_path)
: path.full_path;
Comment thread
pitrou marked this conversation as resolved.
if (status.ok()) {
return FileInfo(path.full_path, type);
return FileInfo(canonical, type);
}
using ::google::cloud::StatusCode;
if (status.code() == StatusCode::kNotFound) {
return FileInfo(path.full_path, FileType::NotFound);
return FileInfo(canonical, FileType::NotFound);
}
return internal::ToArrowStatus(status);
}
Expand DownExpand Up@@ -373,7 +435,9 @@ Result<FileInfoVector> GcsFileSystem::GetFileInfo(const FileSelector& select) {
}

Status GcsFileSystem::CreateDir(const std::string& path, bool recursive) {
return Status::NotImplemented("The GCS FileSystem is not fully implemented");
ARROW_ASSIGN_OR_RAISE(auto p, GcsPath::FromString(path));
if (!recursive) return impl_->CreateDir(p);
return impl_->CreateDirRecursive(p);
}

Status GcsFileSystem::DeleteDir(const std::string& path) {
Expand Down
52 changes: 36 additions & 16 deletions cpp/src/arrow/filesystem/gcsfs.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,24 +40,43 @@ struct ARROW_EXPORT GcsOptions {
bool Equals(const GcsOptions& other) const;
};

// - TODO(ARROW-1231) - review this documentation before closing the bug.
/// \brief GCS-backed FileSystem implementation.
///
/// Some implementation notes:
/// - TODO(ARROW-1231) - review all the notes once completed.
/// - buckets are treated as top-level directories on a "root".
/// - GCS buckets are in a global namespace, only one bucket
/// named `foo` exists in Google Cloud.
/// - Creating new top-level directories is implemented by creating
/// a bucket, this may be a slower operation than usual.
/// - A principal (service account, user, etc) can only list the
/// buckets for a single project, but can access the buckets
/// for many projects. It is possible that listing "all"
/// the buckets returns fewer buckets than you have access to.
/// - GCS does not have directories, they are emulated in this
/// library by listing objects with a common prefix.
/// - In general, GCS has much higher latency than local filesystems.
/// The throughput of GCS is comparable to the throughput of
/// a local file system.
/// GCS (Google Cloud Storage - https://cloud.google.com/storage) is a scalable object
/// storage system for any amount of data. The main abstractions in GCS are buckets and
/// objects. A bucket is a namespace for objects, buckets can store any number of objects,
/// tens of millions and even billions is not uncommon. Each object contains a single
/// blob of data, up to 5TiB in size. Buckets are typically configured to keep a single
/// version of each object, but versioning can be enabled. Versioning is important because
/// objects are immutable, once created one cannot append data to the object or modify the
/// object data in any way.
///
/// GCS buckets are in a global namespace, if a Google Cloud customer creates a bucket
/// named `foo` no other customer can create a bucket with the same name. Note that a
/// principal (a user or service account) may only list the buckets they are entitled to,
/// and then only within a project. It is not possible to list "all" the buckets.
///
/// Within each bucket objects are in flat namespace. GCS does not have folders or
/// directories. However, following some conventions it is possible to emulate
/// directories. To this end, this class:
///
/// - All buckets are treated as directories at the "root"
/// - Creating a root directory results in a new bucket being created, this may be slower
/// than most GCS operations.
/// - Any object with a name ending with a slash (`/`) character is treated as a
/// directory.
/// - The class creates marker objects for a directory, using a trailing slash in the
/// marker names. For debugging purposes, the metadata of these marker objects indicate
/// that they are markers created by this class. The class does not rely on this
/// annotation.
/// - GCS can list all the objects with a given prefix, this is used to emulate listing
/// of directories.
/// - In object lists GCS can summarize all the objects with a common prefix as a single
/// entry, this is used to emulate non-recursive lists. Note that GCS list time is
/// proportional to the number of objects in the prefix. Listing recursively takes
/// almost the same time as non-recursive lists.
///
class ARROW_EXPORT GcsFileSystem : public FileSystem {
public:
~GcsFileSystem() override = default;
Expand All@@ -75,6 +94,7 @@ class ARROW_EXPORT GcsFileSystem : public FileSystem {

Status DeleteDirContents(const std::string& path) override;

/// This is not implemented in GcsFileSystem, as it would be too dangerous.
Status DeleteRootDirContents() override;

Status DeleteFile(const std::string& path) override;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ARROW-14917: [C++] Implement GcsFileSystem::CreateDir by coryan · Pull Request #11842 · apache/arrow · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions cpp/src/arrow/filesystem/gcsfs.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ namespace {

namespace gcs = google::cloud::storage;

auto constexpr kSep = '/';
// Change the default upload buffer size. In general, sending larger buffers is more
// efficient with GCS, as each buffer requires a roundtrip to the service. With formatted
// output (when using `operator<<`), keeping a larger buffer in memory before uploading
Expand All@@ -49,18 +48,17 @@ struct GcsPath {
std::string object;

static Result<GcsPath> FromString(const std::string& s) {
const auto src = internal::RemoveTrailingSlash(s);
auto const first_sep = src.find_first_of(kSep);
auto const first_sep = s.find_first_of(internal::kSep);
if (first_sep == 0) {
return Status::Invalid("Path cannot start with a separator ('", s, "')");
}
if (first_sep == std::string::npos) {
return GcsPath{std::string(src), std::string(src), ""};
return GcsPath{s, internal::RemoveTrailingSlash(s).to_string(), ""};
}
GcsPath path;
path.full_path = std::string(src);
path.bucket = std::string(src.substr(0, first_sep));
path.object = std::string(src.substr(first_sep + 1));
path.full_path = s;
path.bucket = s.substr(0, first_sep);
path.object = s.substr(first_sep + 1);
return path;
}

Expand DownExpand Up@@ -275,12 +273,73 @@ class GcsFileSystem::Impl {
const GcsOptions& options() const { return options_; }

Result<FileInfo> GetFileInfo(const GcsPath& path) {
if (!path.object.empty()) {
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::File);
if (path.object.empty()) {
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
}
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(
path, std::move(meta).status(),
path.object.back() == '/' ? FileType::Directory : FileType::File);
}

// GCS does not have directories or folders. But folders can be emulated (with some
// limitations) using marker objects. That and listing with prefixes creates the
// illusion of folders.
google::cloud::Status CreateDirMarker(const std::string& bucket,
util::string_view name) {
// Make the name canonical.
const auto canonical = internal::EnsureTrailingSlash(name);
return client_
.InsertObject(bucket, canonical, std::string(),
gcs::WithObjectMetadata(gcs::ObjectMetadata().upsert_metadata(
"arrow/gcsfs", "directory")))
.status();
}

google::cloud::Status CreateDirMarkerRecursive(const std::string& bucket,
const std::string& object) {
using GcsCode = google::cloud::StatusCode;
auto get_parent = [](std::string const& path) {
return std::move(internal::GetAbstractPathParent(path).first);
};
// Maybe counterintuitively we create the markers from the most nested and up. Because
// GCS does not have directories creating `a/b/c` will succeed, even if `a/` or `a/b/`
// does not exist. In the common case, where `a/b/` may already exist, it is more
// efficient to just create `a/b/c/` and then find out that `a/b/` was already there.
// In the case where none exists, it does not matter which order we follow.
auto status = CreateDirMarker(bucket, object);
if (status.code() == GcsCode::kAlreadyExists) return {};
if (status.code() == GcsCode::kNotFound) {
// Missing bucket, create it first ...
status = client_.CreateBucket(bucket, gcs::BucketMetadata()).status();
if (status.code() != GcsCode::kOk && status.code() != GcsCode::kAlreadyExists) {
return status;
}
}

for (auto parent = get_parent(object); !parent.empty(); parent = get_parent(parent)) {
status = CreateDirMarker(bucket, parent);
if (status.code() == GcsCode::kAlreadyExists) {
break;
}
if (!status.ok()) {
return status;
}
}
return {};
}

Status CreateDir(const GcsPath& p) {
if (p.object.empty()) {
return internal::ToArrowStatus(
client_.CreateBucket(p.bucket, gcs::BucketMetadata()).status());
}
return internal::ToArrowStatus(CreateDirMarker(p.bucket, p.object));
}

Status CreateDirRecursive(const GcsPath& p) {
return internal::ToArrowStatus(CreateDirMarkerRecursive(p.bucket, p.object));
}

Status DeleteFile(const GcsPath& p) {
Expand DownExpand Up@@ -332,12 +391,15 @@ class GcsFileSystem::Impl {
static Result<FileInfo> GetFileInfoImpl(const GcsPath& path,
const google::cloud::Status& status,
FileType type) {
const auto& canonical = type == FileType::Directory
? internal::EnsureTrailingSlash(path.full_path)
: path.full_path;
Comment thread
pitrou marked this conversation as resolved.
if (status.ok()) {
return FileInfo(path.full_path, type);
return FileInfo(canonical, type);
}
using ::google::cloud::StatusCode;
if (status.code() == StatusCode::kNotFound) {
return FileInfo(path.full_path, FileType::NotFound);
return FileInfo(canonical, FileType::NotFound);
}
return internal::ToArrowStatus(status);
}
Expand DownExpand Up@@ -373,7 +435,9 @@ Result<FileInfoVector> GcsFileSystem::GetFileInfo(const FileSelector& select) {
}

Status GcsFileSystem::CreateDir(const std::string& path, bool recursive) {
return Status::NotImplemented("The GCS FileSystem is not fully implemented");
ARROW_ASSIGN_OR_RAISE(auto p, GcsPath::FromString(path));
if (!recursive) return impl_->CreateDir(p);
return impl_->CreateDirRecursive(p);
}

Status GcsFileSystem::DeleteDir(const std::string& path) {
Expand Down
52 changes: 36 additions & 16 deletions cpp/src/arrow/filesystem/gcsfs.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,24 +40,43 @@ struct ARROW_EXPORT GcsOptions {
bool Equals(const GcsOptions& other) const;
};

// - TODO(ARROW-1231) - review this documentation before closing the bug.
/// \brief GCS-backed FileSystem implementation.
///
/// Some implementation notes:
/// - TODO(ARROW-1231) - review all the notes once completed.
/// - buckets are treated as top-level directories on a "root".
/// - GCS buckets are in a global namespace, only one bucket
/// named `foo` exists in Google Cloud.
/// - Creating new top-level directories is implemented by creating
/// a bucket, this may be a slower operation than usual.
/// - A principal (service account, user, etc) can only list the
/// buckets for a single project, but can access the buckets
/// for many projects. It is possible that listing "all"
/// the buckets returns fewer buckets than you have access to.
/// - GCS does not have directories, they are emulated in this
/// library by listing objects with a common prefix.
/// - In general, GCS has much higher latency than local filesystems.
/// The throughput of GCS is comparable to the throughput of
/// a local file system.
/// GCS (Google Cloud Storage - https://cloud.google.com/storage) is a scalable object
/// storage system for any amount of data. The main abstractions in GCS are buckets and
/// objects. A bucket is a namespace for objects, buckets can store any number of objects,
/// tens of millions and even billions is not uncommon. Each object contains a single
/// blob of data, up to 5TiB in size. Buckets are typically configured to keep a single
/// version of each object, but versioning can be enabled. Versioning is important because
/// objects are immutable, once created one cannot append data to the object or modify the
/// object data in any way.
///
/// GCS buckets are in a global namespace, if a Google Cloud customer creates a bucket
/// named `foo` no other customer can create a bucket with the same name. Note that a
/// principal (a user or service account) may only list the buckets they are entitled to,
/// and then only within a project. It is not possible to list "all" the buckets.
///
/// Within each bucket objects are in flat namespace. GCS does not have folders or
/// directories. However, following some conventions it is possible to emulate
/// directories. To this end, this class:
///
/// - All buckets are treated as directories at the "root"
/// - Creating a root directory results in a new bucket being created, this may be slower
/// than most GCS operations.
/// - Any object with a name ending with a slash (`/`) character is treated as a
/// directory.
/// - The class creates marker objects for a directory, using a trailing slash in the
/// marker names. For debugging purposes, the metadata of these marker objects indicate
/// that they are markers created by this class. The class does not rely on this
/// annotation.
/// - GCS can list all the objects with a given prefix, this is used to emulate listing
/// of directories.
/// - In object lists GCS can summarize all the objects with a common prefix as a single
/// entry, this is used to emulate non-recursive lists. Note that GCS list time is
/// proportional to the number of objects in the prefix. Listing recursively takes
/// almost the same time as non-recursive lists.
///
class ARROW_EXPORT GcsFileSystem : public FileSystem {
public:
~GcsFileSystem() override = default;
Expand All@@ -75,6 +94,7 @@ class ARROW_EXPORT GcsFileSystem : public FileSystem {

Status DeleteDirContents(const std::string& path) override;

/// This is not implemented in GcsFileSystem, as it would be too dangerous.
Status DeleteRootDirContents() override;

Status DeleteFile(const std::string& path) override;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' ARROW-14917: [C++] Implement GcsFileSystem::CreateDir by coryan · Pull Request #11842 · apache/arrow · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions cpp/src/arrow/filesystem/gcsfs.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ namespace {

namespace gcs = google::cloud::storage;

auto constexpr kSep = '/';
// Change the default upload buffer size. In general, sending larger buffers is more
// efficient with GCS, as each buffer requires a roundtrip to the service. With formatted
// output (when using `operator<<`), keeping a larger buffer in memory before uploading
Expand All@@ -49,18 +48,17 @@ struct GcsPath {
std::string object;

static Result<GcsPath> FromString(const std::string& s) {
const auto src = internal::RemoveTrailingSlash(s);
auto const first_sep = src.find_first_of(kSep);
auto const first_sep = s.find_first_of(internal::kSep);
if (first_sep == 0) {
return Status::Invalid("Path cannot start with a separator ('", s, "')");
}
if (first_sep == std::string::npos) {
return GcsPath{std::string(src), std::string(src), ""};
return GcsPath{s, internal::RemoveTrailingSlash(s).to_string(), ""};
}
GcsPath path;
path.full_path = std::string(src);
path.bucket = std::string(src.substr(0, first_sep));
path.object = std::string(src.substr(first_sep + 1));
path.full_path = s;
path.bucket = s.substr(0, first_sep);
path.object = s.substr(first_sep + 1);
return path;
}

Expand DownExpand Up@@ -275,12 +273,73 @@ class GcsFileSystem::Impl {
const GcsOptions& options() const { return options_; }

Result<FileInfo> GetFileInfo(const GcsPath& path) {
if (!path.object.empty()) {
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::File);
if (path.object.empty()) {
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
}
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(
path, std::move(meta).status(),
path.object.back() == '/' ? FileType::Directory : FileType::File);
}

// GCS does not have directories or folders. But folders can be emulated (with some
// limitations) using marker objects. That and listing with prefixes creates the
// illusion of folders.
google::cloud::Status CreateDirMarker(const std::string& bucket,
util::string_view name) {
// Make the name canonical.
const auto canonical = internal::EnsureTrailingSlash(name);
return client_
.InsertObject(bucket, canonical, std::string(),
gcs::WithObjectMetadata(gcs::ObjectMetadata().upsert_metadata(
"arrow/gcsfs", "directory")))
.status();
}

google::cloud::Status CreateDirMarkerRecursive(const std::string& bucket,
const std::string& object) {
using GcsCode = google::cloud::StatusCode;
auto get_parent = [](std::string const& path) {
return std::move(internal::GetAbstractPathParent(path).first);
};
// Maybe counterintuitively we create the markers from the most nested and up. Because
// GCS does not have directories creating `a/b/c` will succeed, even if `a/` or `a/b/`
// does not exist. In the common case, where `a/b/` may already exist, it is more
// efficient to just create `a/b/c/` and then find out that `a/b/` was already there.
// In the case where none exists, it does not matter which order we follow.
auto status = CreateDirMarker(bucket, object);
if (status.code() == GcsCode::kAlreadyExists) return {};
if (status.code() == GcsCode::kNotFound) {
// Missing bucket, create it first ...
status = client_.CreateBucket(bucket, gcs::BucketMetadata()).status();
if (status.code() != GcsCode::kOk && status.code() != GcsCode::kAlreadyExists) {
return status;
}
}

for (auto parent = get_parent(object); !parent.empty(); parent = get_parent(parent)) {
status = CreateDirMarker(bucket, parent);
if (status.code() == GcsCode::kAlreadyExists) {
break;
}
if (!status.ok()) {
return status;
}
}
return {};
}

Status CreateDir(const GcsPath& p) {
if (p.object.empty()) {
return internal::ToArrowStatus(
client_.CreateBucket(p.bucket, gcs::BucketMetadata()).status());
}
return internal::ToArrowStatus(CreateDirMarker(p.bucket, p.object));
}

Status CreateDirRecursive(const GcsPath& p) {
return internal::ToArrowStatus(CreateDirMarkerRecursive(p.bucket, p.object));
}

Status DeleteFile(const GcsPath& p) {
Expand DownExpand Up@@ -332,12 +391,15 @@ class GcsFileSystem::Impl {
static Result<FileInfo> GetFileInfoImpl(const GcsPath& path,
const google::cloud::Status& status,
FileType type) {
const auto& canonical = type == FileType::Directory
? internal::EnsureTrailingSlash(path.full_path)
: path.full_path;
Comment thread
pitrou marked this conversation as resolved.
if (status.ok()) {
return FileInfo(path.full_path, type);
return FileInfo(canonical, type);
}
using ::google::cloud::StatusCode;
if (status.code() == StatusCode::kNotFound) {
return FileInfo(path.full_path, FileType::NotFound);
return FileInfo(canonical, FileType::NotFound);
}
return internal::ToArrowStatus(status);
}
Expand DownExpand Up@@ -373,7 +435,9 @@ Result<FileInfoVector> GcsFileSystem::GetFileInfo(const FileSelector& select) {
}

Status GcsFileSystem::CreateDir(const std::string& path, bool recursive) {
return Status::NotImplemented("The GCS FileSystem is not fully implemented");
ARROW_ASSIGN_OR_RAISE(auto p, GcsPath::FromString(path));
if (!recursive) return impl_->CreateDir(p);
return impl_->CreateDirRecursive(p);
}

Status GcsFileSystem::DeleteDir(const std::string& path) {
Expand Down
52 changes: 36 additions & 16 deletions cpp/src/arrow/filesystem/gcsfs.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,24 +40,43 @@ struct ARROW_EXPORT GcsOptions {
bool Equals(const GcsOptions& other) const;
};

// - TODO(ARROW-1231) - review this documentation before closing the bug.
/// \brief GCS-backed FileSystem implementation.
///
/// Some implementation notes:
/// - TODO(ARROW-1231) - review all the notes once completed.
/// - buckets are treated as top-level directories on a "root".
/// - GCS buckets are in a global namespace, only one bucket
/// named `foo` exists in Google Cloud.
/// - Creating new top-level directories is implemented by creating
/// a bucket, this may be a slower operation than usual.
/// - A principal (service account, user, etc) can only list the
/// buckets for a single project, but can access the buckets
/// for many projects. It is possible that listing "all"
/// the buckets returns fewer buckets than you have access to.
/// - GCS does not have directories, they are emulated in this
/// library by listing objects with a common prefix.
/// - In general, GCS has much higher latency than local filesystems.
/// The throughput of GCS is comparable to the throughput of
/// a local file system.
/// GCS (Google Cloud Storage - https://cloud.google.com/storage) is a scalable object
/// storage system for any amount of data. The main abstractions in GCS are buckets and
/// objects. A bucket is a namespace for objects, buckets can store any number of objects,
/// tens of millions and even billions is not uncommon. Each object contains a single
/// blob of data, up to 5TiB in size. Buckets are typically configured to keep a single
/// version of each object, but versioning can be enabled. Versioning is important because
/// objects are immutable, once created one cannot append data to the object or modify the
/// object data in any way.
///
/// GCS buckets are in a global namespace, if a Google Cloud customer creates a bucket
/// named `foo` no other customer can create a bucket with the same name. Note that a
/// principal (a user or service account) may only list the buckets they are entitled to,
/// and then only within a project. It is not possible to list "all" the buckets.
///
/// Within each bucket objects are in flat namespace. GCS does not have folders or
/// directories. However, following some conventions it is possible to emulate
/// directories. To this end, this class:
///
/// - All buckets are treated as directories at the "root"
/// - Creating a root directory results in a new bucket being created, this may be slower
/// than most GCS operations.
/// - Any object with a name ending with a slash (`/`) character is treated as a
/// directory.
/// - The class creates marker objects for a directory, using a trailing slash in the
/// marker names. For debugging purposes, the metadata of these marker objects indicate
/// that they are markers created by this class. The class does not rely on this
/// annotation.
/// - GCS can list all the objects with a given prefix, this is used to emulate listing
/// of directories.
/// - In object lists GCS can summarize all the objects with a common prefix as a single
/// entry, this is used to emulate non-recursive lists. Note that GCS list time is
/// proportional to the number of objects in the prefix. Listing recursively takes
/// almost the same time as non-recursive lists.
///
class ARROW_EXPORT GcsFileSystem : public FileSystem {
public:
~GcsFileSystem() override = default;
Expand All@@ -75,6 +94,7 @@ class ARROW_EXPORT GcsFileSystem : public FileSystem {

Status DeleteDirContents(const std::string& path) override;

/// This is not implemented in GcsFileSystem, as it would be too dangerous.
Status DeleteRootDirContents() override;

Status DeleteFile(const std::string& path) override;
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); ARROW-14917: [C++] Implement GcsFileSystem::CreateDir by coryan · Pull Request #11842 · apache/arrow · GitHub
Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions cpp/src/arrow/filesystem/gcsfs.cc
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,7 +34,6 @@ namespace {

namespace gcs = google::cloud::storage;

auto constexpr kSep = '/';
// Change the default upload buffer size. In general, sending larger buffers is more
// efficient with GCS, as each buffer requires a roundtrip to the service. With formatted
// output (when using `operator<<`), keeping a larger buffer in memory before uploading
Expand All@@ -49,18 +48,17 @@ struct GcsPath {
std::string object;

static Result<GcsPath> FromString(const std::string& s) {
const auto src = internal::RemoveTrailingSlash(s);
auto const first_sep = src.find_first_of(kSep);
auto const first_sep = s.find_first_of(internal::kSep);
if (first_sep == 0) {
return Status::Invalid("Path cannot start with a separator ('", s, "')");
}
if (first_sep == std::string::npos) {
return GcsPath{std::string(src), std::string(src), ""};
return GcsPath{s, internal::RemoveTrailingSlash(s).to_string(), ""};
}
GcsPath path;
path.full_path = std::string(src);
path.bucket = std::string(src.substr(0, first_sep));
path.object = std::string(src.substr(first_sep + 1));
path.full_path = s;
path.bucket = s.substr(0, first_sep);
path.object = s.substr(first_sep + 1);
return path;
}

Expand DownExpand Up@@ -275,12 +273,73 @@ class GcsFileSystem::Impl {
const GcsOptions& options() const { return options_; }

Result<FileInfo> GetFileInfo(const GcsPath& path) {
if (!path.object.empty()) {
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::File);
if (path.object.empty()) {
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
}
auto meta = client_.GetBucketMetadata(path.bucket);
return GetFileInfoImpl(path, std::move(meta).status(), FileType::Directory);
auto meta = client_.GetObjectMetadata(path.bucket, path.object);
return GetFileInfoImpl(
path, std::move(meta).status(),
path.object.back() == '/' ? FileType::Directory : FileType::File);
}

// GCS does not have directories or folders. But folders can be emulated (with some
// limitations) using marker objects. That and listing with prefixes creates the
// illusion of folders.
google::cloud::Status CreateDirMarker(const std::string& bucket,
util::string_view name) {
// Make the name canonical.
const auto canonical = internal::EnsureTrailingSlash(name);
return client_
.InsertObject(bucket, canonical, std::string(),
gcs::WithObjectMetadata(gcs::ObjectMetadata().upsert_metadata(
"arrow/gcsfs", "directory")))
.status();
}

google::cloud::Status CreateDirMarkerRecursive(const std::string& bucket,
const std::string& object) {
using GcsCode = google::cloud::StatusCode;
auto get_parent = [](std::string const& path) {
return std::move(internal::GetAbstractPathParent(path).first);
};
// Maybe counterintuitively we create the markers from the most nested and up. Because
// GCS does not have directories creating `a/b/c` will succeed, even if `a/` or `a/b/`
// does not exist. In the common case, where `a/b/` may already exist, it is more
// efficient to just create `a/b/c/` and then find out that `a/b/` was already there.
// In the case where none exists, it does not matter which order we follow.
auto status = CreateDirMarker(bucket, object);
if (status.code() == GcsCode::kAlreadyExists) return {};
if (status.code() == GcsCode::kNotFound) {
// Missing bucket, create it first ...
status = client_.CreateBucket(bucket, gcs::BucketMetadata()).status();
if (status.code() != GcsCode::kOk && status.code() != GcsCode::kAlreadyExists) {
return status;
}
}

for (auto parent = get_parent(object); !parent.empty(); parent = get_parent(parent)) {
status = CreateDirMarker(bucket, parent);
if (status.code() == GcsCode::kAlreadyExists) {
break;
}
if (!status.ok()) {
return status;
}
}
return {};
}

Status CreateDir(const GcsPath& p) {
if (p.object.empty()) {
return internal::ToArrowStatus(
client_.CreateBucket(p.bucket, gcs::BucketMetadata()).status());
}
return internal::ToArrowStatus(CreateDirMarker(p.bucket, p.object));
}

Status CreateDirRecursive(const GcsPath& p) {
return internal::ToArrowStatus(CreateDirMarkerRecursive(p.bucket, p.object));
}

Status DeleteFile(const GcsPath& p) {
Expand DownExpand Up@@ -332,12 +391,15 @@ class GcsFileSystem::Impl {
static Result<FileInfo> GetFileInfoImpl(const GcsPath& path,
const google::cloud::Status& status,
FileType type) {
const auto& canonical = type == FileType::Directory
? internal::EnsureTrailingSlash(path.full_path)
: path.full_path;
Comment thread
pitrou marked this conversation as resolved.
if (status.ok()) {
return FileInfo(path.full_path, type);
return FileInfo(canonical, type);
}
using ::google::cloud::StatusCode;
if (status.code() == StatusCode::kNotFound) {
return FileInfo(path.full_path, FileType::NotFound);
return FileInfo(canonical, FileType::NotFound);
}
return internal::ToArrowStatus(status);
}
Expand DownExpand Up@@ -373,7 +435,9 @@ Result<FileInfoVector> GcsFileSystem::GetFileInfo(const FileSelector& select) {
}

Status GcsFileSystem::CreateDir(const std::string& path, bool recursive) {
return Status::NotImplemented("The GCS FileSystem is not fully implemented");
ARROW_ASSIGN_OR_RAISE(auto p, GcsPath::FromString(path));
if (!recursive) return impl_->CreateDir(p);
return impl_->CreateDirRecursive(p);
}

Status GcsFileSystem::DeleteDir(const std::string& path) {
Expand Down
52 changes: 36 additions & 16 deletions cpp/src/arrow/filesystem/gcsfs.h
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,24 +40,43 @@ struct ARROW_EXPORT GcsOptions {
bool Equals(const GcsOptions& other) const;
};

// - TODO(ARROW-1231) - review this documentation before closing the bug.
/// \brief GCS-backed FileSystem implementation.
///
/// Some implementation notes:
/// - TODO(ARROW-1231) - review all the notes once completed.
/// - buckets are treated as top-level directories on a "root".
/// - GCS buckets are in a global namespace, only one bucket
/// named `foo` exists in Google Cloud.
/// - Creating new top-level directories is implemented by creating
/// a bucket, this may be a slower operation than usual.
/// - A principal (service account, user, etc) can only list the
/// buckets for a single project, but can access the buckets
/// for many projects. It is possible that listing "all"
/// the buckets returns fewer buckets than you have access to.
/// - GCS does not have directories, they are emulated in this
/// library by listing objects with a common prefix.
/// - In general, GCS has much higher latency than local filesystems.
/// The throughput of GCS is comparable to the throughput of
/// a local file system.
/// GCS (Google Cloud Storage - https://cloud.google.com/storage) is a scalable object
/// storage system for any amount of data. The main abstractions in GCS are buckets and
/// objects. A bucket is a namespace for objects, buckets can store any number of objects,
/// tens of millions and even billions is not uncommon. Each object contains a single
/// blob of data, up to 5TiB in size. Buckets are typically configured to keep a single
/// version of each object, but versioning can be enabled. Versioning is important because
/// objects are immutable, once created one cannot append data to the object or modify the
/// object data in any way.
///
/// GCS buckets are in a global namespace, if a Google Cloud customer creates a bucket
/// named `foo` no other customer can create a bucket with the same name. Note that a
/// principal (a user or service account) may only list the buckets they are entitled to,
/// and then only within a project. It is not possible to list "all" the buckets.
///
/// Within each bucket objects are in flat namespace. GCS does not have folders or
/// directories. However, following some conventions it is possible to emulate
/// directories. To this end, this class:
///
/// - All buckets are treated as directories at the "root"
/// - Creating a root directory results in a new bucket being created, this may be slower
/// than most GCS operations.
/// - Any object with a name ending with a slash (`/`) character is treated as a
/// directory.
/// - The class creates marker objects for a directory, using a trailing slash in the
/// marker names. For debugging purposes, the metadata of these marker objects indicate
/// that they are markers created by this class. The class does not rely on this
/// annotation.
/// - GCS can list all the objects with a given prefix, this is used to emulate listing
/// of directories.
/// - In object lists GCS can summarize all the objects with a common prefix as a single
/// entry, this is used to emulate non-recursive lists. Note that GCS list time is
/// proportional to the number of objects in the prefix. Listing recursively takes
/// almost the same time as non-recursive lists.
///
class ARROW_EXPORT GcsFileSystem : public FileSystem {
public:
~GcsFileSystem() override = default;
Expand All@@ -75,6 +94,7 @@ class ARROW_EXPORT GcsFileSystem : public FileSystem {

Status DeleteDirContents(const std::string& path) override;

/// This is not implemented in GcsFileSystem, as it would be too dangerous.
Status DeleteRootDirContents() override;

Status DeleteFile(const std::string& path) override;
Expand Down
Loading