diff --git a/cmake/external/leveldb.cmake b/cmake/external/leveldb.cmake index bb6763a299..2596d355bf 100644 --- a/cmake/external/leveldb.cmake +++ b/cmake/external/leveldb.cmake @@ -18,9 +18,10 @@ if(TARGET leveldb) return() endif() +set(current_patch_file "") # Explicitly initialize for this scope if (DESKTOP AND MSVC) -set(patch_file - ${CMAKE_CURRENT_LIST_DIR}/../../scripts/git/patches/leveldb/0001-leveldb-1.23-windows-paths.patch) + set(current_patch_file + ${CMAKE_CURRENT_LIST_DIR}/../../scripts/git/patches/leveldb/0001-leveldb-1.23-windows-paths.patch) endif() # This version must be kept in sync with the version in firestore.patch.txt. @@ -28,6 +29,11 @@ endif() # firestore.patch.txt accordingly. set(version 1.23) +set(final_patch_command "") +if(current_patch_file) + set(final_patch_command git apply ${current_patch_file} && git gc --aggressive) +endif() + ExternalProject_Add( leveldb @@ -42,5 +48,5 @@ ExternalProject_Add( INSTALL_COMMAND "" TEST_COMMAND "" HTTP_HEADER "${EXTERNAL_PROJECT_HTTP_HEADER}" - PATCH_COMMAND git apply ${patch_file} && git gc --aggressive + PATCH_COMMAND ${final_patch_command} ) diff --git a/scripts/print_workflow_run_errors.py b/scripts/print_workflow_run_errors.py new file mode 100644 index 0000000000..6e4a80116c --- /dev/null +++ b/scripts/print_workflow_run_errors.py @@ -0,0 +1,467 @@ +#!/usr/bin/env python3 +# Copyright 2024 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Fetches and prints errors from a GitHub Workflow run.""" + +import argparse +import os +import sys +import datetime +import requests +import json +import re +import subprocess +from requests.adapters import HTTPAdapter +from requests.packages.urllib3.util.retry import Retry + +# Constants for GitHub API interaction +RETRIES = 3 +BACKOFF = 5 +RETRY_STATUS = (403, 500, 502, 504) # HTTP status codes to retry on +TIMEOUT = 10 # Default timeout for requests in seconds +LONG_TIMEOUT = 30 # Timeout for potentially longer requests like log downloads + +# Global variables for the target repository, populated by set_repo_info() +OWNER = '' +REPO = '' +BASE_URL = 'https://api.github.com' +GITHUB_API_URL = '' + + +def set_repo_info(owner_name, repo_name): + """Sets the global repository owner, name, and API URL.""" + global OWNER, REPO, GITHUB_API_URL + OWNER = owner_name + REPO = repo_name + GITHUB_API_URL = f'{BASE_URL}/repos/{OWNER}/{REPO}' + return True + + +def requests_retry_session(retries=RETRIES, + backoff_factor=BACKOFF, + status_forcelist=RETRY_STATUS): + """Creates a requests session with retry logic.""" + session = requests.Session() + retry = Retry(total=retries, + read=retries, + connect=retries, + backoff_factor=backoff_factor, + status_forcelist=status_forcelist) + adapter = HTTPAdapter(max_retries=retry) + session.mount('http://', adapter) + session.mount('https://', adapter) + return session + + +def get_current_branch_name(): + """Gets the current git branch name.""" + try: + branch_bytes = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], stderr=subprocess.PIPE) + return branch_bytes.decode().strip() + except (subprocess.CalledProcessError, FileNotFoundError, UnicodeDecodeError) as e: + sys.stderr.write(f"Info: Could not determine current git branch via 'git rev-parse --abbrev-ref HEAD': {e}. Branch will need to be specified.\n") + return None + except Exception as e: # Catch any other unexpected error. + sys.stderr.write(f"Info: An unexpected error occurred while determining current git branch: {e}. Branch will need to be specified.\n") + return None + + +def main(): + """Main function to parse arguments and orchestrate the script.""" + determined_owner = None + determined_repo = None + try: + git_url_bytes = subprocess.check_output(["git", "remote", "get-url", "origin"], stderr=subprocess.PIPE) + git_url = git_url_bytes.decode().strip() + match = re.search(r"(?:(?:https?://github\.com/)|(?:git@github\.com:))([^/]+)/([^/.]+)(?:\.git)?", git_url) + if match: + determined_owner = match.group(1) + determined_repo = match.group(2) + sys.stderr.write(f"Determined repository: {determined_owner}/{determined_repo} from git remote 'origin'.\n") + except (subprocess.CalledProcessError, FileNotFoundError, UnicodeDecodeError) as e: + sys.stderr.write(f"Could not automatically determine repository from git remote 'origin': {e}\n") + except Exception as e: + sys.stderr.write(f"An unexpected error occurred while determining repository: {e}\n") + + def parse_repo_url_arg(url_string): + """Parses owner and repository name from various GitHub URL formats.""" + url_match = re.search(r"(?:(?:https?://github\.com/)|(?:git@github\.com:))([^/]+)/([^/.]+?)(?:\.git)?/?$", url_string) + if url_match: + return url_match.group(1), url_match.group(2) + return None, None + + current_branch = get_current_branch_name() + + parser = argparse.ArgumentParser( + description="Fetch and display failed steps and their logs from a GitHub workflow run.", + formatter_class=argparse.RawTextHelpFormatter + ) + parser.add_argument( + "--workflow", "--workflow-name", + type=str, + default="integration_test.yml", + help="Name of the workflow file (e.g., 'main.yml' or 'build-test.yml'). Default: 'integration_test.yml'." + ) + parser.add_argument( + "--branch", + type=str, + default=current_branch, + help=f"GitHub branch name to check for the workflow run. {'Default: ' + current_branch if current_branch else 'Required if not determinable from current git branch.'}" + ) + parser.add_argument( + "--url", + type=str, + default=None, + help="Full GitHub repository URL (e.g., https://github.com/owner/repo or git@github.com:owner/repo.git). Takes precedence over --owner/--repo." + ) + parser.add_argument( + "--owner", + type=str, + default=determined_owner, + help=f"Repository owner. Used if --url is not provided. {'Default: ' + determined_owner if determined_owner else 'Required if --url is not used and not determinable from git.'}" + ) + parser.add_argument( + "--repo", + type=str, + default=determined_repo, + help=f"Repository name. Used if --url is not provided. {'Default: ' + determined_repo if determined_repo else 'Required if --url is not used and not determinable from git.'}" + ) + parser.add_argument( + "--token", + type=str, + default=os.environ.get("GITHUB_TOKEN"), + help="GitHub token. Can also be set via GITHUB_TOKEN env var or from ~/.github_token." + ) + parser.add_argument( + "--log-lines", + type=int, + default=100, + help="Number of lines to print from the end of each failed step's log. Default: 100." + ) + parser.add_argument( + "--all-failed-steps", + action="store_true", + default=False, + help="If set, print logs for all failed steps in a job. Default is to print logs only for the first failed step." + ) + parser.add_argument( + "--grep-pattern", "-g", + type=str, + default=None, + help="Extended Regular Expression (ERE) to search for in logs. If provided, log output will be filtered by grep." + ) + parser.add_argument( + "--grep-context", "-C", + type=int, + default=5, + help="Number of lines of leading and trailing context to print for grep matches. Default: 5." + ) + + args = parser.parse_args() + error_suffix = " (use --help for more details)" + + token = args.token + if not token: + try: + with open(os.path.expanduser("~/.github_token"), "r") as f: + token = f.read().strip() + if token: + sys.stderr.write("Using token from ~/.github_token\n") + except FileNotFoundError: + pass + except Exception as e: + sys.stderr.write(f"Warning: Could not read ~/.github_token: {e}\n") + + if not token: + sys.stderr.write(f"Error: GitHub token not provided. Set GITHUB_TOKEN, use --token, or place it in ~/.github_token.{error_suffix}\n") + sys.exit(1) + args.token = token # Ensure args.token is populated + + final_owner = None + final_repo = None + + if args.url: + owner_explicitly_set_via_arg = args.owner is not None and args.owner != determined_owner + repo_explicitly_set_via_arg = args.repo is not None and args.repo != determined_repo + if owner_explicitly_set_via_arg or repo_explicitly_set_via_arg: + sys.stderr.write(f"Error: Cannot use --owner or --repo when --url is specified.{error_suffix}\n") + sys.exit(1) + + parsed_owner, parsed_repo = parse_repo_url_arg(args.url) + if parsed_owner and parsed_repo: + final_owner = parsed_owner + final_repo = parsed_repo + sys.stderr.write(f"Using repository from --url: {final_owner}/{final_repo}\n") + else: + sys.stderr.write(f"Error: Invalid URL format: {args.url}. Expected https://github.com/owner/repo or git@github.com:owner/repo.git{error_suffix}\n") + sys.exit(1) + else: + is_owner_from_user_arg = args.owner is not None and args.owner != determined_owner + is_repo_from_user_arg = args.repo is not None and args.repo != determined_repo + + if is_owner_from_user_arg or is_repo_from_user_arg: # User explicitly set at least one of owner/repo via args + if args.owner and args.repo: + final_owner = args.owner + final_repo = args.repo + sys.stderr.write(f"Using repository from --owner/--repo args: {final_owner}/{final_repo}\n") + else: + sys.stderr.write(f"Error: Both --owner and --repo must be specified if one is provided explicitly (and --url is not used).{error_suffix}\n") + sys.exit(1) + elif args.owner and args.repo: # Both args have values, likely from successful auto-detection (or user provided matching defaults) + final_owner = args.owner + final_repo = args.repo + # No specific message needed if it's from auto-detection, already printed. + # If user explicitly provided args that match auto-detected, that's fine. + # If final_owner/repo are still None here, it means auto-detection failed AND user provided nothing for owner/repo. + # Or, only one of owner/repo was auto-detected and the other wasn't provided. + + if not final_owner or not final_repo: + missing_parts = [] + if not final_owner: missing_parts.append("--owner") + if not final_repo: missing_parts.append("--repo") + + error_msg = "Error: Could not determine repository." + if missing_parts: + error_msg += f" Missing { ' and '.join(missing_parts) }." + error_msg += f" Please specify --url, OR both --owner and --repo, OR ensure git remote 'origin' is configured correctly.{error_suffix}" + sys.stderr.write(error_msg + "\n") + sys.exit(1) + + if not set_repo_info(final_owner, final_repo): + # This path should ideally not be reached if final_owner/repo are validated, + # but as a safeguard: + sys.stderr.write(f"Error: Could not set repository info to {final_owner}/{final_repo}. Ensure owner/repo are correct.{error_suffix}\n") + sys.exit(1) + + if not args.branch: + sys.stderr.write(f"Error: Branch name is required. Please specify --branch or ensure it can be detected from your current git repository.{error_suffix}\n") + sys.exit(1) + + sys.stderr.write(f"Processing workflow '{args.workflow}' on branch '{args.branch}' for repo {OWNER}/{REPO}\n") + + run = get_latest_workflow_run(args.token, args.workflow, args.branch) + if not run: + sys.stderr.write(f"No workflow run found for workflow '{args.workflow}' on branch '{args.branch}'.\n") + sys.exit(0) + + sys.stderr.write(f"Found workflow run ID: {run['id']} (Status: {run.get('status')}, Conclusion: {run.get('conclusion')})\n") + + failed_jobs = get_failed_jobs_for_run(args.token, run['id']) + + if not failed_jobs: + sys.stderr.write(f"No failed jobs found for workflow run ID: {run['id']}.\n") + if run.get('conclusion') == 'success': + print(f"Workflow run {run['id']} completed successfully with no failed jobs.") + elif run.get('status') == 'in_progress' and run.get('conclusion') is None: + print(f"Workflow run {run['id']} is still in progress. No failed jobs reported yet.") + else: + # This case might indicate the workflow failed but not at a job level, + # or jobs are still pending/running. + print(f"Workflow run {run['id']} has conclusion '{run.get('conclusion')}' but no specific failed jobs were identified by this script's criteria.") + sys.exit(0) + + print(f"\n--- Failed Jobs for Workflow Run ID: {run['id']} ({run.get('html_url', 'No URL')}) ---\n") + + for job in failed_jobs: + print(f"==================================================================================") + print(f"Job: {job['name']} (ID: {job['id']}) - FAILED") + print(f"Job URL: {job.get('html_url', 'N/A')}") + print(f"==================================================================================") + + job_logs = get_job_logs(args.token, job['id']) + if not job_logs: + print("Could not retrieve logs for this job.") + continue + + failed_steps_details = [] + if job.get('steps'): + for step in job['steps']: + if step.get('conclusion') == 'failure': + failed_steps_details.append(step) + + if not failed_steps_details: + print("\nNo specific failed steps found in job data, but job marked as failed. Printing last lines of full job log as fallback:\n") + log_lines = job_logs.splitlines() + for line in log_lines[-args.log_lines:]: + print(line) + print("\n--- End of log snippet for job ---") + continue + + print(f"\n--- Failed Steps in Job: {job['name']} ---") + first_failed_step_logged = False + for step in failed_steps_details: + if not args.all_failed_steps and first_failed_step_logged: + print(f"\n--- Skipping subsequent failed step: {step.get('name', 'Unnamed step')} (use --all-failed-steps to see all) ---") + break # Stop after the first failed step if not --all-failed-steps + + step_name = step.get('name', 'Unnamed step') + print(f"\n--- Step: {step_name} ---") + + # Crude log extraction: + # Regex to match group start, attempting to capture the step name robustly + escaped_step_name = re.escape(step_name) + step_start_pattern = re.compile(r"^##\[group\](?:Run\s+|Setup\s+|Complete\s+)?.*?" + escaped_step_name, re.IGNORECASE) + step_end_pattern = re.compile(r"^##\[endgroup\]") + + current_step_log_segment = [] + capturing_for_failed_step = False + log_lines_for_job = job_logs.splitlines() # Split once per job + + # Try to find the specific step's log segment + for line in log_lines_for_job: + if step_start_pattern.search(line): + capturing_for_failed_step = True + current_step_log_segment = [line] # Start with the group line + continue + if capturing_for_failed_step: + current_step_log_segment.append(line) + if step_end_pattern.search(line): + capturing_for_failed_step = False + # Found the end of the targeted step's log + break # Stop processing lines for this step (within this job's logs) + + log_to_process = "" + log_source_message = "" + + if current_step_log_segment: + log_to_process = "\n".join(current_step_log_segment) + log_source_message = f"Log for failed step '{step_name}'" + else: + log_to_process = "\n".join(log_lines_for_job) # Use the full job log as fallback + log_source_message = f"Could not isolate log for step '{step_name}'. Using entire job log" + + if args.grep_pattern: + print(f"{log_source_message} (grep results for pattern '{args.grep_pattern}' with context {args.grep_context}):") + try: + # Using subprocess to call grep + # Pass log_to_process as stdin to grep + process = subprocess.run( + ['grep', '-E', f"-C{args.grep_context}", args.grep_pattern], + input=log_to_process, + text=True, + capture_output=True, + check=False # Do not throw exception on non-zero exit (e.g. no match) + ) + if process.returncode == 0: # Match found + print(process.stdout.strip()) + elif process.returncode == 1: # No match found + print(f"No matches found for pattern '{args.grep_pattern}' in this log segment.") + else: # Grep error + sys.stderr.write(f"Grep command failed with error code {process.returncode}:\n{process.stderr}\n") + except FileNotFoundError: + sys.stderr.write("Error: 'grep' command not found. Please ensure it is installed and in your PATH to use --grep-pattern.\n") + # Fallback to printing last N lines if grep is not found? Or just skip log? For now, skip. + print("Skipping log display for this step as grep is unavailable.") + except Exception as e: + sys.stderr.write(f"An unexpected error occurred while running grep: {e}\n") + print("Skipping log display due to an error with grep.") + else: + # Default behavior: print last N lines + print(f"{log_source_message} (last {args.log_lines} lines):") + # current_step_log_segment is a list of lines, log_lines_for_job is also a list of lines + lines_to_print_from = current_step_log_segment if current_step_log_segment else log_lines_for_job + for log_line in lines_to_print_from[-args.log_lines:]: + print(log_line) + + print(f"--- End of log for step: {step_name} ---") + first_failed_step_logged = True # Mark that we've logged at least one step + + print(f"\n--- End of Failed Steps for Job: {job['name']} ---\n") + + +def get_latest_workflow_run(token, workflow_name, branch_name): + """Fetches the most recent workflow run for a given workflow name and branch.""" + url = f'{GITHUB_API_URL}/actions/workflows/{workflow_name}/runs' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + params = {'branch': branch_name, 'per_page': 1, 'page': 1} # Get the most recent 1 + + try: + with requests_retry_session().get(url, headers=headers, params=params, timeout=TIMEOUT) as response: + response.raise_for_status() + data = response.json() + if data['workflow_runs'] and len(data['workflow_runs']) > 0: + return data['workflow_runs'][0] # The first one is the most recent + else: + return None + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to fetch workflow runs for '{workflow_name}' on branch '{branch_name}': {e}\n") + if e.response is not None: + sys.stderr.write(f"Response content: {e.response.text}\n") + return None + except json.JSONDecodeError as e: + sys.stderr.write(f"Error: Failed to parse JSON response for workflow runs: {e}\n") + return None + + +def get_failed_jobs_for_run(token, run_id): + """Fetches all jobs for a given workflow run and filters for failed ones.""" + url = f'{GITHUB_API_URL}/actions/runs/{run_id}/jobs' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + + page = 1 + per_page = 100 # GitHub API default and max is 100 for many paginated endpoints + all_jobs = [] + + while True: + params = {'per_page': per_page, 'page': page, 'filter': 'latest'} # 'latest' attempt for each job + try: + with requests_retry_session().get(url, headers=headers, params=params, timeout=TIMEOUT) as response: + response.raise_for_status() + data = response.json() + current_page_jobs = data.get('jobs', []) + if not current_page_jobs: + break + all_jobs.extend(current_page_jobs) + if len(current_page_jobs) < per_page: + break # Reached last page + page += 1 + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to fetch jobs for run ID {run_id} (page {page}): {e}\n") + if e.response is not None: + sys.stderr.write(f"Response content: {e.response.text}\n") + return None # Return None if any page fails + except json.JSONDecodeError as e: + sys.stderr.write(f"Error: Failed to parse JSON response for jobs: {e}\n") + return None + + failed_jobs = [job for job in all_jobs if job.get('conclusion') == 'failure'] + return failed_jobs + + +def get_job_logs(token, job_id): + """Downloads the logs for a specific job.""" + url = f'{GITHUB_API_URL}/actions/jobs/{job_id}/logs' + headers = {'Accept': 'application/vnd.github.v3+json', 'Authorization': f'token {token}'} + + try: + # Logs can be large, use a longer timeout and stream if necessary, + # but for typical use, direct content might be fine. + # The GitHub API for logs redirects to a download URL. `requests` handles this. + with requests_retry_session().get(url, headers=headers, timeout=LONG_TIMEOUT, stream=False) as response: + response.raise_for_status() + # The response for logs is plain text, not JSON + return response.text + except requests.exceptions.RequestException as e: + sys.stderr.write(f"Error: Failed to download logs for job ID {job_id}: {e}\n") + if e.response is not None: + # Log URLs might expire or have other issues, content might be HTML error page + sys.stderr.write(f"Response status: {e.response.status_code}\n") + # Avoid printing potentially huge HTML error pages to stderr directly + # sys.stderr.write(f"Response content: {e.response.text[:500]}...\n") # Print a snippet + return None + + +if __name__ == "__main__": + main() diff --git a/storage/CMakeLists.txt b/storage/CMakeLists.txt index e2fd88e72f..e4a722fd9f 100644 --- a/storage/CMakeLists.txt +++ b/storage/CMakeLists.txt @@ -19,6 +19,7 @@ set(common_SRCS src/common/common.cc src/common/controller.cc src/common/listener.cc + src/common/list_result.cc src/common/metadata.cc src/common/storage.cc src/common/storage_reference.cc @@ -36,6 +37,7 @@ binary_to_array("storage_resources" set(android_SRCS ${storage_resources_source} src/android/controller_android.cc + src/android/list_result_android.cc src/android/metadata_android.cc src/android/storage_android.cc src/android/storage_reference_android.cc) @@ -44,6 +46,7 @@ set(android_SRCS set(ios_SRCS src/ios/controller_ios.mm src/ios/listener_ios.mm + src/ios/list_result_ios.mm src/ios/metadata_ios.mm src/ios/storage_ios.mm src/ios/storage_reference_ios.mm @@ -54,6 +57,7 @@ set(desktop_SRCS src/desktop/controller_desktop.cc src/desktop/curl_requests.cc src/desktop/listener_desktop.cc + src/desktop/list_result_desktop.cc src/desktop/metadata_desktop.cc src/desktop/rest_operation.cc src/desktop/storage_desktop.cc diff --git a/storage/integration_test/src/integration_test.cc b/storage/integration_test/src/integration_test.cc index f430de37de..8f305993c2 100644 --- a/storage/integration_test/src/integration_test.cc +++ b/storage/integration_test/src/integration_test.cc @@ -20,6 +20,7 @@ #include #include #include // NOLINT +#include // For std::vector in list tests #include "app_framework.h" // NOLINT #include "firebase/app.h" @@ -80,6 +81,8 @@ using app_framework::PathForResource; using app_framework::ProcessEvents; using firebase_test_framework::FirebaseTest; using testing::ElementsAreArray; +using testing::IsEmpty; +using testing::UnorderedElementsAreArray; class FirebaseStorageTest : public FirebaseTest { public: @@ -96,7 +99,6 @@ class FirebaseStorageTest : public FirebaseTest { // Called after each test. void TearDown() override; - // File references that we need to delete on test exit. protected: // Initialize Firebase App and Firebase Auth. static void InitializeAppAndAuth(); @@ -118,6 +120,17 @@ class FirebaseStorageTest : public FirebaseTest { // Create a unique working folder and return a reference to it. firebase::storage::StorageReference CreateFolder(); + // Uploads a string as a file to the given StorageReference. + void UploadStringAsFile(firebase::storage::StorageReference& ref, + const std::string& content, + const char* content_type = nullptr); + + // Verifies the contents of a ListResult. + void VerifyListResultContains( + const firebase::storage::ListResult& list_result, + const std::vector& expected_item_names, + const std::vector& expected_prefix_names); + static firebase::App* shared_app_; static firebase::auth::Auth* shared_auth_; @@ -212,6 +225,7 @@ void FirebaseStorageTest::TerminateAppAndAuth() { void FirebaseStorageTest::SetUp() { FirebaseTest::SetUp(); InitializeStorage(); + // list_test_root_ removed from SetUp } void FirebaseStorageTest::TearDown() { @@ -313,6 +327,65 @@ void FirebaseStorageTest::SignOut() { EXPECT_FALSE(shared_auth_->current_user().is_valid()); } +void FirebaseStorageTest::UploadStringAsFile( + firebase::storage::StorageReference& ref, const std::string& content, + const char* content_type) { + LogDebug("Uploading string content to: gs://%s%s", ref.bucket().c_str(), + ref.full_path().c_str()); + firebase::storage::Metadata metadata; + if (content_type) { + metadata.set_content_type(content_type); + } + firebase::Future future = + RunWithRetry([&]() { + return ref.PutBytes(content.c_str(), content.length(), metadata); + }); + WaitForCompletion(future, "UploadStringAsFile"); + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << "Failed to upload to " << ref.full_path() << ": " + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + // On some platforms (iOS), size_bytes might not be immediately available or + // might be 0 if the upload was very fast and metadata propagation is slow. + // For small files, this is less critical than the content being there. + // For larger files in other tests, size_bytes is asserted. + // ASSERT_EQ(future.result()->size_bytes(), content.length()); + cleanup_files_.push_back(ref); +} + +void FirebaseStorageTest::VerifyListResultContains( + const firebase::storage::ListResult& list_result, + const std::vector& expected_item_names, + const std::vector& expected_prefix_names) { + ASSERT_TRUE(list_result.is_valid()); + + std::vector actual_item_names; + for (const auto& item_ref : list_result.items()) { + actual_item_names.push_back(item_ref.name()); + } + std::sort(actual_item_names.begin(), actual_item_names.end()); + std::vector sorted_expected_item_names = expected_item_names; + std::sort(sorted_expected_item_names.begin(), + sorted_expected_item_names.end()); + + EXPECT_THAT(actual_item_names, + ::testing::ContainerEq(sorted_expected_item_names)) + << "Item names do not match expected."; + + std::vector actual_prefix_names; + for (const auto& prefix_ref : list_result.prefixes()) { + actual_prefix_names.push_back(prefix_ref.name()); + } + std::sort(actual_prefix_names.begin(), actual_prefix_names.end()); + std::vector sorted_expected_prefix_names = expected_prefix_names; + std::sort(sorted_expected_prefix_names.begin(), + sorted_expected_prefix_names.end()); + + EXPECT_THAT(actual_prefix_names, + ::testing::ContainerEq(sorted_expected_prefix_names)) + << "Prefix names do not match expected."; +} + firebase::storage::StorageReference FirebaseStorageTest::CreateFolder() { // Generate a folder for the test data based on the time in milliseconds. int64_t time_in_microseconds = GetCurrentTimeInMicroseconds(); @@ -1622,4 +1695,213 @@ TEST_F(FirebaseStorageTest, TestInvalidatingReferencesWhenDeletingApp) { InitializeAppAndAuth(); } +TEST_F(FirebaseStorageTest, ListAllBasic) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // Removed + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_all_basic_root"); + ASSERT_TRUE(test_root.is_valid()) + << "Test root for ListAllBasic is not valid."; + + UploadStringAsFile(test_root.Child("file_a.txt"), "content_a"); + UploadStringAsFile(test_root.Child("file_b.txt"), "content_b"); + UploadStringAsFile(test_root.Child("prefix1/file_c.txt"), + "content_c_in_prefix1"); + UploadStringAsFile(test_root.Child("prefix2/file_e.txt"), + "content_e_in_prefix2"); + + LogDebug("Calling ListAll() on gs://%s%s", test_root.bucket().c_str(), + test_root.full_path().c_str()); + firebase::Future future = test_root.ListAll(); + WaitForCompletion(future, "ListAllBasic"); + + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + + VerifyListResultContains(*result, {"file_a.txt", "file_b.txt"}, + {"prefix1/", "prefix2/"}); + EXPECT_TRUE(result->page_token().empty()) + << "Page token should be empty for ListAll."; +} + +TEST_F(FirebaseStorageTest, ListPaginated) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // Removed + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_paginated_root"); + ASSERT_TRUE(test_root.is_valid()) + << "Test root for ListPaginated is not valid."; + + // Expected total entries: file_aa.txt, file_bb.txt, file_ee.txt, prefix_x/, + // prefix_y/ (5 entries) + UploadStringAsFile(test_root.Child("file_aa.txt"), "content_aa"); + UploadStringAsFile(test_root.Child("prefix_x/file_cc.txt"), + "content_cc_in_prefix_x"); + UploadStringAsFile(test_root.Child("file_bb.txt"), "content_bb"); + UploadStringAsFile(test_root.Child("prefix_y/file_dd.txt"), + "content_dd_in_prefix_y"); + UploadStringAsFile(test_root.Child("file_ee.txt"), "content_ee"); + + std::vector all_item_names_collected; + std::vector all_prefix_names_collected; + std::string page_token = ""; + const int page_size = 2; + int page_count = 0; + const int max_pages = 5; // Safety break for loop + + LogDebug("Starting paginated List() on gs://%s%s with page_size %d", + test_root.bucket().c_str(), test_root.full_path().c_str(), + page_size); + + do { + page_count++; + LogDebug("Fetching page %d, token: '%s'", page_count, page_token.c_str()); + firebase::Future future = + page_token.empty() ? test_root.List(page_size) + : test_root.List(page_size, page_token.c_str()); + WaitForCompletion(future, + "ListPaginated - Page " + std::to_string(page_count)); + + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + ASSERT_TRUE(result->is_valid()); + + LogDebug("Page %d items: %zu, prefixes: %zu", page_count, + result->items().size(), result->prefixes().size()); + for (const auto& item : result->items()) { + all_item_names_collected.push_back(item.name()); + LogDebug(" Item: %s", item.name().c_str()); + } + for (const auto& prefix : result->prefixes()) { + all_prefix_names_collected.push_back(prefix.name()); + LogDebug(" Prefix: %s", prefix.name().c_str()); + } + + page_token = result->page_token(); + + size_t entries_on_page = result->items().size() + result->prefixes().size(); + + if (!page_token.empty()) { + EXPECT_EQ(entries_on_page, page_size) + << "A non-last page should have full page_size entries."; + } else { + // This is the last page + size_t total_entries = 5; + size_t expected_entries_on_last_page = total_entries % page_size; + if (expected_entries_on_last_page == 0 && + total_entries > 0) { // if total is a multiple of page_size + expected_entries_on_last_page = page_size; + } + EXPECT_EQ(entries_on_page, expected_entries_on_last_page); + } + } while (!page_token.empty() && page_count < max_pages); + + EXPECT_LT(page_count, max_pages) + << "Exceeded max_pages, possible infinite loop."; + EXPECT_EQ(page_count, (5 + page_size - 1) / page_size) + << "Unexpected number of pages."; + + std::vector expected_final_items = {"file_aa.txt", "file_bb.txt", + "file_ee.txt"}; + std::vector expected_final_prefixes = {"prefix_x/", "prefix_y/"}; + + // VerifyListResultContains needs a ListResult object. We can't directly use + // it with collected names. Instead, we sort and compare the collected names. + std::sort(all_item_names_collected.begin(), all_item_names_collected.end()); + std::sort(all_prefix_names_collected.begin(), + all_prefix_names_collected.end()); + std::sort(expected_final_items.begin(), expected_final_items.end()); + std::sort(expected_final_prefixes.begin(), expected_final_prefixes.end()); + + EXPECT_THAT(all_item_names_collected, + ::testing::ContainerEq(expected_final_items)); + EXPECT_THAT(all_prefix_names_collected, + ::testing::ContainerEq(expected_final_prefixes)); +} + +TEST_F(FirebaseStorageTest, ListEmpty) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed as it's a lightweight + // test. + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_empty_root"); + ASSERT_TRUE(test_root.is_valid()) << "Test root for ListEmpty is not valid."; + + // Do not upload anything to test_root. + + LogDebug("Calling ListAll() on empty folder: gs://%s%s", + test_root.bucket().c_str(), test_root.full_path().c_str()); + firebase::Future future = test_root.ListAll(); + WaitForCompletion(future, "ListEmpty"); + + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + + VerifyListResultContains(*result, {}, {}); + EXPECT_TRUE(result->page_token().empty()); +} + +TEST_F(FirebaseStorageTest, ListWithMaxResultsGreaterThanActual) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed. + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_max_greater_root"); + ASSERT_TRUE(test_root.is_valid()) + << "Test root for ListWithMaxResultsGreaterThanActual is not valid."; + + UploadStringAsFile(test_root.Child("only_file.txt"), "content_only"); + UploadStringAsFile(test_root.Child("only_prefix/another.txt"), + "content_another_in_prefix"); + + LogDebug("Calling List(10) on gs://%s%s", test_root.bucket().c_str(), + test_root.full_path().c_str()); + firebase::Future future = + test_root.List(10); // Max results (10) > actual (1 file + 1 prefix = 2) + WaitForCompletion(future, "ListWithMaxResultsGreaterThanActual"); + + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + + VerifyListResultContains(*result, {"only_file.txt"}, {"only_prefix/"}); + EXPECT_TRUE(result->page_token().empty()); +} + +TEST_F(FirebaseStorageTest, ListNonExistentPath) { + // SKIP_TEST_ON_ANDROID_EMULATOR; // No skip needed. + SignIn(); + firebase::storage::StorageReference test_root = + CreateFolder().Child("list_non_existent_parent_root"); + ASSERT_TRUE(test_root.is_valid()) + << "Test root for ListNonExistentPath is not valid."; + + firebase::storage::StorageReference non_existent_ref = + test_root.Child("this_folder_truly_does_not_exist"); + // No cleanup needed as nothing is created. + + LogDebug("Calling ListAll() on non-existent path: gs://%s%s", + non_existent_ref.bucket().c_str(), + non_existent_ref.full_path().c_str()); + firebase::Future future = + non_existent_ref.ListAll(); + WaitForCompletion(future, "ListNonExistentPath"); + + // Listing a non-existent path should not be an error, it's just an empty + // list. + ASSERT_EQ(future.error(), firebase::storage::kErrorNone) + << future.error_message(); + ASSERT_NE(future.result(), nullptr); + const firebase::storage::ListResult* result = future.result(); + + VerifyListResultContains(*result, {}, {}); + EXPECT_TRUE(result->page_token().empty()); +} + } // namespace firebase_testapp_automated diff --git a/storage/src/android/list_result_android.cc b/storage/src/android/list_result_android.cc new file mode 100644 index 0000000000..84a6157192 --- /dev/null +++ b/storage/src/android/list_result_android.cc @@ -0,0 +1,243 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "storage/src/android/list_result_android.h" + +#include "app/src/include/firebase/app.h" +#include "app/src/util_android.h" +#include "storage/src/android/storage_android.h" +#include "storage/src/android/storage_reference_android.h" +#include "storage/src/common/common_android.h" + +namespace firebase { +namespace storage { +namespace internal { + +// clang-format off +#define LIST_RESULT_METHODS(X) \ + X(GetItems, "getItems", "()Ljava/util/List;"), \ + X(GetPrefixes, "getPrefixes", "()Ljava/util/List;"), \ + X(GetPageToken, "getPageToken", "()Ljava/lang/String;") +// clang-format on +METHOD_LOOKUP_DECLARATION(list_result, LIST_RESULT_METHODS) +METHOD_LOOKUP_DEFINITION(list_result, "com/google/firebase/storage/ListResult", + LIST_RESULT_METHODS) + +// clang-format off +#define JAVA_LIST_METHODS(X) \ + X(Size, "size", "()I"), \ + X(Get, "get", "(I)Ljava/lang/Object;") +// clang-format on +METHOD_LOOKUP_DECLARATION(java_list, JAVA_LIST_METHODS) +METHOD_LOOKUP_DEFINITION(java_list, "java/util/List", JAVA_LIST_METHODS) + +bool ListResultInternal::Initialize(App* app) { + JNIEnv* env = app->GetJNIEnv(); + if (!list_result::CacheMethodIds(env, app->activity())) { + return false; + } + if (!java_list::CacheMethodIds(env, app->activity())) { + // Release already cached list_result methods if java_list fails. + list_result::ReleaseClass(env); + return false; + } + return true; +} + +void ListResultInternal::Terminate(App* app) { + JNIEnv* env = app->GetJNIEnv(); + list_result::ReleaseClass(env); + java_list::ReleaseClass(env); +} + +ListResultInternal::ListResultInternal(StorageInternal* storage_internal, + jobject java_list_result) + : storage_internal_(storage_internal), + list_result_java_ref_(nullptr), + items_converted_(false), + prefixes_converted_(false), + page_token_converted_(false) { + FIREBASE_ASSERT(storage_internal != nullptr); + FIREBASE_ASSERT(java_list_result != nullptr); + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + list_result_java_ref_ = env->NewGlobalRef(java_list_result); +} + +ListResultInternal::ListResultInternal(const ListResultInternal& other) + : storage_internal_(other.storage_internal_), + list_result_java_ref_(nullptr), + items_cache_(other.items_cache_), + prefixes_cache_(other.prefixes_cache_), + page_token_cache_(other.page_token_cache_), + items_converted_(other.items_converted_), + prefixes_converted_(other.prefixes_converted_), + page_token_converted_(other.page_token_converted_) { + FIREBASE_ASSERT(storage_internal_ != nullptr); + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + if (other.list_result_java_ref_ != nullptr) { + list_result_java_ref_ = env->NewGlobalRef(other.list_result_java_ref_); + } +} + +ListResultInternal& ListResultInternal::operator=( + const ListResultInternal& other) { + if (&other == this) { + return *this; + } + storage_internal_ = other.storage_internal_; + FIREBASE_ASSERT(storage_internal_ != nullptr); + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + if (list_result_java_ref_ != nullptr) { + env->DeleteGlobalRef(list_result_java_ref_); + list_result_java_ref_ = nullptr; + } + if (other.list_result_java_ref_ != nullptr) { + list_result_java_ref_ = env->NewGlobalRef(other.list_result_java_ref_); + } + items_cache_ = other.items_cache_; + prefixes_cache_ = other.prefixes_cache_; + page_token_cache_ = other.page_token_cache_; + items_converted_ = other.items_converted_; + prefixes_converted_ = other.prefixes_converted_; + page_token_converted_ = other.page_token_converted_; + return *this; +} + +ListResultInternal::~ListResultInternal() { + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + if (list_result_java_ref_ != nullptr) { + env->DeleteGlobalRef(list_result_java_ref_); + list_result_java_ref_ = nullptr; + } +} + +std::vector ListResultInternal::ProcessJavaReferenceList( + jobject java_list_ref) const { + std::vector cpp_references; + if (java_list_ref == nullptr) { + return cpp_references; + } + + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + jint size = env->CallIntMethod(java_list_ref, + java_list::GetMethodId(java_list::kSize)); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + LogError("Failed to get size of Java List in ListResultInternal"); + return cpp_references; + } + + for (jint i = 0; i < size; ++i) { + jobject java_storage_ref = env->CallObjectMethod( + java_list_ref, java_list::GetMethodId(java_list::kGet), i); + if (env->ExceptionCheck() || java_storage_ref == nullptr) { + env->ExceptionClear(); + LogError( + "Failed to get StorageReference object from Java List at index %d", + i); + if (java_storage_ref) env->DeleteLocalRef(java_storage_ref); + continue; + } + // Create a C++ StorageReferenceInternal from the Java StorageReference. + // StorageReferenceInternal constructor will create a global ref for the + // java obj. + StorageReferenceInternal* sfr_internal = + new StorageReferenceInternal(storage_internal_, java_storage_ref); + cpp_references.push_back(StorageReference(sfr_internal)); + env->DeleteLocalRef(java_storage_ref); + } + return cpp_references; +} + +std::vector ListResultInternal::items() const { + if (!list_result_java_ref_) return items_cache_; // Return empty if no ref + if (items_converted_) { + return items_cache_; + } + + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + jobject java_items_list = env->CallObjectMethod( + list_result_java_ref_, list_result::GetMethodId(list_result::kGetItems)); + if (env->ExceptionCheck() || java_items_list == nullptr) { + env->ExceptionClear(); + LogError("Failed to call getItems() on Java ListResult"); + if (java_items_list) env->DeleteLocalRef(java_items_list); + // In case of error, still mark as "converted" to avoid retrying JNI call, + // return whatever might be in cache (empty at this point). + items_converted_ = true; + return items_cache_; + } + + items_cache_ = ProcessJavaReferenceList(java_items_list); + env->DeleteLocalRef(java_items_list); + items_converted_ = true; + return items_cache_; +} + +std::vector ListResultInternal::prefixes() const { + if (!list_result_java_ref_) return prefixes_cache_; + if (prefixes_converted_) { + return prefixes_cache_; + } + + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + jobject java_prefixes_list = env->CallObjectMethod( + list_result_java_ref_, + list_result::GetMethodId(list_result::kGetPrefixes)); + if (env->ExceptionCheck() || java_prefixes_list == nullptr) { + env->ExceptionClear(); + LogError("Failed to call getPrefixes() on Java ListResult"); + if (java_prefixes_list) env->DeleteLocalRef(java_prefixes_list); + prefixes_converted_ = true; + return prefixes_cache_; + } + + prefixes_cache_ = ProcessJavaReferenceList(java_prefixes_list); + env->DeleteLocalRef(java_prefixes_list); + prefixes_converted_ = true; + return prefixes_cache_; +} + +std::string ListResultInternal::page_token() const { + if (!list_result_java_ref_) return page_token_cache_; + if (page_token_converted_) { + return page_token_cache_; + } + + JNIEnv* env = storage_internal_->app()->GetJNIEnv(); + jstring page_token_jstring = static_cast(env->CallObjectMethod( + list_result_java_ref_, + list_result::GetMethodId(list_result::kGetPageToken))); + if (env->ExceptionCheck()) { + env->ExceptionClear(); + LogError("Failed to call getPageToken() on Java ListResult"); + if (page_token_jstring) env->DeleteLocalRef(page_token_jstring); + page_token_converted_ = true; + return page_token_cache_; // Return empty if error + } + + if (page_token_jstring != nullptr) { + page_token_cache_ = util::JniStringToString(env, page_token_jstring); + env->DeleteLocalRef(page_token_jstring); + } else { + page_token_cache_ = ""; // Explicitly set to empty if Java string is null + } + + page_token_converted_ = true; + return page_token_cache_; +} + +} // namespace internal +} // namespace storage +} // namespace firebase diff --git a/storage/src/android/list_result_android.h b/storage/src/android/list_result_android.h new file mode 100644 index 0000000000..7b9e2c8327 --- /dev/null +++ b/storage/src/android/list_result_android.h @@ -0,0 +1,100 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_ANDROID_LIST_RESULT_ANDROID_H_ +#define FIREBASE_STORAGE_SRC_ANDROID_LIST_RESULT_ANDROID_H_ + +#include + +#include +#include + +#include "app/src/util_android.h" +#include "firebase/app.h" +#include "firebase/storage/storage_reference.h" +#include "storage/src/android/storage_android.h" + +namespace firebase { +namespace storage { + +// Forward declaration for platform-specific ListResultInternal. +class ListResult; + +namespace internal { + +// Contains the Android-specific implementation of ListResultInternal. +class ListResultInternal { + public: + // Constructor. + // + // @param[in] storage_internal Pointer to the StorageInternal object. + // @param[in] java_list_result Java ListResult object. This function will + // retain a global reference to this object. + ListResultInternal(StorageInternal* storage_internal, + jobject java_list_result); + + // Copy constructor. + ListResultInternal(const ListResultInternal& other); + + // Copy assignment operator. + ListResultInternal& operator=(const ListResultInternal& other); + + // Destructor. + ~ListResultInternal(); + + // Gets the items (files) in this result. + std::vector items() const; + + // Gets the prefixes (folders) in this result. + std::vector prefixes() const; + + // Gets the page token for the next page of results. + // Returns an empty string if there are no more results. + std::string page_token() const; + + // Returns the StorageInternal object associated with this ListResult. + StorageInternal* storage_internal() const { return storage_internal_; } + + // Initializes ListResultInternal JNI. + static bool Initialize(App* app); + + // Terminates ListResultInternal JNI. + static void Terminate(App* app); + + private: + friend class firebase::storage::ListResult; + + // Converts a Java List of Java StorageReference objects to a C++ vector of + // C++ StorageReference objects. + std::vector ProcessJavaReferenceList( + jobject java_list_ref) const; + + StorageInternal* storage_internal_; // Not owned. + // Global reference to Java com.google.firebase.storage.ListResult object. + jobject list_result_java_ref_; + + // Caches for converted data + mutable std::vector items_cache_; + mutable std::vector prefixes_cache_; + mutable std::string page_token_cache_; + mutable bool items_converted_; + mutable bool prefixes_converted_; + mutable bool page_token_converted_; +}; + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_ANDROID_LIST_RESULT_ANDROID_H_ diff --git a/storage/src/android/storage_reference_android.cc b/storage/src/android/storage_reference_android.cc index 99b9d40280..ef1c7d155a 100644 --- a/storage/src/android/storage_reference_android.cc +++ b/storage/src/android/storage_reference_android.cc @@ -50,6 +50,12 @@ namespace internal { "()Ljava/lang/String;"), \ X(GetStorage, "getStorage", \ "()Lcom/google/firebase/storage/FirebaseStorage;"), \ + X(List, "list", \ + "(I)Lcom/google/android/gms/tasks/Task;"), \ + X(ListWithPageToken, "list", \ + "(ILjava/lang/String;)Lcom/google/android/gms/tasks/Task;"), \ + X(ListAll, "listAll", \ + "()Lcom/google/android/gms/tasks/Task;"), \ X(PutStream, "putStream", \ "(Ljava/io/InputStream;)Lcom/google/firebase/storage/UploadTask;"), \ X(PutStreamWithMetadata, "putStream", \ @@ -105,17 +111,26 @@ enum StorageReferenceFn { kStorageReferenceFnUpdateMetadata, kStorageReferenceFnPutBytes, kStorageReferenceFnPutFile, + kStorageReferenceFnList, kStorageReferenceFnCount, }; bool StorageReferenceInternal::Initialize(App* app) { JNIEnv* env = app->GetJNIEnv(); jobject activity = app->activity(); - return storage_reference::CacheMethodIds(env, activity); + if (!storage_reference::CacheMethodIds(env, activity)) { + return false; + } + if (!ListResultInternal::Initialize(app)) { + storage_reference::ReleaseClass(env); + return false; + } + return true; } void StorageReferenceInternal::Terminate(App* app) { JNIEnv* env = app->GetJNIEnv(); + ListResultInternal::Terminate(app); storage_reference::ReleaseClass(env); util::CheckAndClearJniExceptions(env); } @@ -309,11 +324,37 @@ void StorageReferenceInternal::FutureCallback(JNIEnv* env, jobject result, file_download_task_task_snapshot::kGetBytesTransferred)); data->impl->Complete(data->handle, kErrorNone, status_message, [bytes](size_t* size) { *size = bytes; }); + } else if (result && env->IsInstanceOf(result, list_result::GetClass())) { + // Complete a Future from a Java ListResult object. + LogDebug("FutureCallback: Completing a Future from a ListResult."); + // Create a local reference for the ListResultInternal constructor + jobject result_ref = env->NewLocalRef(result); + ListResultInternal* list_result_internal_ptr = + new ListResultInternal(data->storage, result_ref); + env->DeleteLocalRef(result_ref); // ListResultInternal made a global ref. + + data->impl->Complete( + data->handle, kErrorNone, status_message, + [list_result_internal_ptr](ListResult* out_list_result) { + *out_list_result = ListResult(list_result_internal_ptr); + }); } else { LogDebug("FutureCallback: Completing a Future from a default result."); // Unknown or null result type, treat this as a Future and just // return success. - data->impl->Complete(data->handle, kErrorNone, status_message); + // This case might need adjustment if List operations that fail end up here + // without a specific exception being caught by result_code check. + if (data->func == kStorageReferenceFnList) { + // If it was a list operation but didn't result in a ListResult object + // (e.g. error not caught as exception) complete with an error and an + // invalid ListResult. + data->impl->CompleteWithResult( + data->handle, kErrorUnknown, + "List operation failed to produce a valid ListResult.", + ListResult(nullptr)); + } else { + data->impl->Complete(data->handle, kErrorNone, status_message); + } } if (data->listener != nullptr) { env->CallVoidMethod(data->listener, @@ -687,6 +728,76 @@ Future StorageReferenceInternal::PutFileLastResult() { future()->LastResult(kStorageReferenceFnPutFile)); } +Future StorageReferenceInternal::List(int32_t max_results) { + JNIEnv* env = storage_->app()->GetJNIEnv(); + ReferenceCountedFutureImpl* future_impl = future(); + FutureHandle handle = future_impl->Alloc(kStorageReferenceFnList); + + jobject task = env->CallObjectMethod( + obj_, storage_reference::GetMethodId(storage_reference::kList), + static_cast(max_results)); + + util::RegisterCallbackOnTask( + env, task, FutureCallback, + new FutureCallbackData(handle, future_impl, storage_, + kStorageReferenceFnList), + storage_->jni_task_id()); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(task); + return ListLastResult(); +} + +Future StorageReferenceInternal::List(int32_t max_results, + const char* page_token) { + JNIEnv* env = storage_->app()->GetJNIEnv(); + ReferenceCountedFutureImpl* future_impl = future(); + FutureHandle handle = future_impl->Alloc(kStorageReferenceFnList); + + jstring page_token_jstring = + page_token ? env->NewStringUTF(page_token) : nullptr; + + jobject task = env->CallObjectMethod( + obj_, + storage_reference::GetMethodId(storage_reference::kListWithPageToken), + static_cast(max_results), page_token_jstring); + + if (page_token_jstring) { + env->DeleteLocalRef(page_token_jstring); + } + + util::RegisterCallbackOnTask( + env, task, FutureCallback, + new FutureCallbackData(handle, future_impl, storage_, + kStorageReferenceFnList), + storage_->jni_task_id()); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(task); + return ListLastResult(); +} + +Future StorageReferenceInternal::ListAll() { + JNIEnv* env = storage_->app()->GetJNIEnv(); + ReferenceCountedFutureImpl* future_impl = future(); + FutureHandle handle = future_impl->Alloc(kStorageReferenceFnList); + + jobject task = env->CallObjectMethod( + obj_, storage_reference::GetMethodId(storage_reference::kListAll)); + + util::RegisterCallbackOnTask( + env, task, FutureCallback, + new FutureCallbackData(handle, future_impl, storage_, + kStorageReferenceFnList), + storage_->jni_task_id()); + util::CheckAndClearJniExceptions(env); + env->DeleteLocalRef(task); + return ListLastResult(); +} + +Future StorageReferenceInternal::ListLastResult() { + return static_cast&>( + future()->LastResult(kStorageReferenceFnList)); +} + ReferenceCountedFutureImpl* StorageReferenceInternal::future() { return storage_->future_manager().GetFutureApi(this); } diff --git a/storage/src/android/storage_reference_android.h b/storage/src/android/storage_reference_android.h index 6643a4d8bd..8eae3d2287 100644 --- a/storage/src/android/storage_reference_android.h +++ b/storage/src/android/storage_reference_android.h @@ -129,6 +129,16 @@ class StorageReferenceInternal { // Returns the result of the most recent call to PutFile(); Future PutFileLastResult(); + // Asynchronously lists objects and common prefixes under this reference. + Future List(int32_t max_results); + Future List(int32_t max_results, const char* page_token); + + // Asynchronously lists all objects and common prefixes under this reference. + Future ListAll(); + + // Returns the result of the most recent List operation. + Future ListLastResult(); + // Initialize JNI bindings for this class. static bool Initialize(App* app); static void Terminate(App* app); diff --git a/storage/src/common/list_result.cc b/storage/src/common/list_result.cc new file mode 100644 index 0000000000..f904058839 --- /dev/null +++ b/storage/src/common/list_result.cc @@ -0,0 +1,198 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "storage/src/include/firebase/storage/list_result.h" + +#include + +#include "app/src/cleanup_notifier.h" +#include "app/src/include/firebase/app.h" +#include "app/src/util.h" +#include "storage/src/common/storage_reference_internal.h" + +// Platform specific ListResultInternal definitions +#if FIREBASE_PLATFORM_ANDROID +#include "storage/src/android/list_result_android.h" +#include "storage/src/android/storage_android.h" +#elif FIREBASE_PLATFORM_IOS +#include "storage/src/ios/list_result_ios.h" +#include "storage/src/ios/storage_ios.h" +#elif FIREBASE_PLATFORM_DESKTOP +#include "storage/src/desktop/list_result_desktop.h" +#include "storage/src/desktop/storage_desktop.h" +#endif // FIREBASE_PLATFORM_ANDROID, FIREBASE_PLATFORM_IOS, + // FIREBASE_PLATFORM_DESKTOP + +namespace firebase { +namespace storage { + +using internal::ListResultInternal; + +// Global function to be called by CleanupNotifier +// This function is responsible for cleaning up the internal state of a +// ListResult object when the App is being shut down. +void GlobalCleanupListResult(void* list_result_void) { + if (list_result_void) { + ListResult* list_result = static_cast(list_result_void); + // This method will delete internal_ and set it to nullptr. + list_result->ClearInternalForCleanup(); + } +} + +ListResult::ListResult() : internal_(nullptr) {} + +ListResult::ListResult(internal::ListResultInternal* internal) + : internal_(internal) { + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } +} + +ListResult::ListResult(const ListResult& other) : internal_(nullptr) { + if (other.internal_) { + internal_ = new internal::ListResultInternal(*other.internal_); + } + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } +} + +ListResult& ListResult::operator=(const ListResult& other) { + if (this == &other) { + return *this; + } + + // Unregister and delete current internal object + if (internal_) { + if (internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().UnregisterObject(this); + } + delete internal_; + internal_ = nullptr; + } + + // Copy from other + if (other.internal_) { + internal_ = new internal::ListResultInternal(*other.internal_); + } + + // Register new internal object + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } + return *this; +} + +ListResult::ListResult(ListResult&& other) : internal_(nullptr) { + // Unregister 'other' as it will no longer manage its internal_ + if (other.internal_ && other.internal_->storage_internal() && + other.internal_->storage_internal()->app_valid()) { + other.internal_->storage_internal()->cleanup().UnregisterObject(&other); + } + + // Move internal pointer + internal_ = other.internal_; + other.internal_ = nullptr; + + // Register 'this' if it now owns a valid internal object + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } +} + +ListResult& ListResult::operator=(ListResult&& other) { + if (this == &other) { + return *this; + } + + // Unregister and delete current internal object for 'this' + if (internal_) { + if (internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().UnregisterObject(this); + } + delete internal_; + internal_ = nullptr; + } + + // Unregister 'other' as it will no longer manage its internal_ + if (other.internal_ && other.internal_->storage_internal() && + other.internal_->storage_internal()->app_valid()) { + other.internal_->storage_internal()->cleanup().UnregisterObject(&other); + } + + // Move internal pointer + internal_ = other.internal_; + other.internal_ = nullptr; + + // Register 'this' if it now owns a valid internal object + if (internal_ && internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().RegisterObject( + this, GlobalCleanupListResult); + } + return *this; +} + +ListResult::~ListResult() { + if (internal_) { + if (internal_->storage_internal() && + internal_->storage_internal()->app_valid()) { + internal_->storage_internal()->cleanup().UnregisterObject(this); + } + delete internal_; + internal_ = nullptr; + } +} + +void ListResult::ClearInternalForCleanup() { + // This method is called by GlobalCleanupListResult. + // The object is already unregistered from the CleanupNotifier by the notifier + // itself before this callback is invoked. So, no need to call + // UnregisterObject here. + delete internal_; + internal_ = nullptr; +} + +std::vector ListResult::items() const { + assert(internal_ != nullptr); + if (!internal_) return std::vector(); + return internal_->items(); +} + +std::vector ListResult::prefixes() const { + assert(internal_ != nullptr); + if (!internal_) return std::vector(); + return internal_->prefixes(); +} + +std::string ListResult::page_token() const { + assert(internal_ != nullptr); + if (!internal_) return ""; + return internal_->page_token(); +} + +bool ListResult::is_valid() const { return internal_ != nullptr; } + +} // namespace storage +} // namespace firebase diff --git a/storage/src/common/storage_reference.cc b/storage/src/common/storage_reference.cc index 54bc6983b7..847e3174d6 100644 --- a/storage/src/common/storage_reference.cc +++ b/storage/src/common/storage_reference.cc @@ -15,6 +15,8 @@ #include "storage/src/include/firebase/storage/storage_reference.h" #include "app/src/assert.h" +#include "firebase/storage/list_result.h" +#include "storage/src/include/firebase/storage/future_details.h" #ifdef __APPLE__ #include "TargetConditionals.h" @@ -85,15 +87,14 @@ StorageReference::StorageReference(StorageReferenceInternal* internal) } StorageReference::StorageReference(const StorageReference& other) - : internal_(other.internal_ ? new StorageReferenceInternal(*other.internal_) - : nullptr) { + : internal_(other.internal_ ? other.internal_->Clone() : nullptr) { StorageReferenceInternalCommon::RegisterForCleanup(this, internal_); } StorageReference& StorageReference::operator=(const StorageReference& other) { + if (this == &other) return *this; StorageReferenceInternalCommon::DeleteInternal(this); - internal_ = other.internal_ ? new StorageReferenceInternal(*other.internal_) - : nullptr; + internal_ = other.internal_ ? other.internal_->Clone() : nullptr; StorageReferenceInternalCommon::RegisterForCleanup(this, internal_); return *this; } @@ -248,5 +249,27 @@ Future StorageReference::PutFileLastResult() { bool StorageReference::is_valid() const { return internal_ != nullptr; } +Future StorageReference::List(int32_t max_results) { + if (!internal_) return Future(); + return internal_->List(max_results); +} + +Future StorageReference::List(int32_t max_results, + const char* page_token) { + if (!internal_) return Future(); + // Pass an empty string if page_token is nullptr, as internal methods + // might expect a non-null, though possibly empty, string. + return internal_->List(max_results, page_token ? page_token : ""); +} + +Future StorageReference::ListAll() { + if (!internal_) return Future(); + return internal_->ListAll(); +} + +Future StorageReference::ListLastResult() { + return internal_ ? internal_->ListLastResult() : Future(); +} + } // namespace storage } // namespace firebase diff --git a/storage/src/common/storage_reference_internal.h b/storage/src/common/storage_reference_internal.h new file mode 100644 index 0000000000..8d85a4a3b6 --- /dev/null +++ b/storage/src/common/storage_reference_internal.h @@ -0,0 +1,104 @@ +#ifndef FIREBASE_STORAGE_SRC_COMMON_STORAGE_REFERENCE_INTERNAL_H_ +#define FIREBASE_STORAGE_SRC_COMMON_STORAGE_REFERENCE_INTERNAL_H_ + +#include +#include + +#include "firebase/future.h" +#include "firebase/storage/metadata.h" +// Listener and Controller are used in method signatures, need full declaration or forward +#include "firebase/storage/listener.h" +#include "firebase/storage/controller.h" + +namespace firebase { +namespace storage { + +// Forward declarations from public API +class Storage; +class ListResult; +class StorageReference; + + +namespace internal { + +class StorageInternal; // Platform-specific internal helper for Storage. + +// Defines the common internal interface for StorageReference. +// Platform-specific implementations (Desktop, Android, iOS) will derive from this. +class StorageReferenceInternal { + public: + virtual ~StorageReferenceInternal() = default; + + // Interface methods mirroring the public StorageReference API + virtual Storage* storage() const = 0; + virtual StorageReferenceInternal* Child(const char* path) const = 0; + virtual std::string bucket() const = 0; + virtual std::string full_path() const = 0; + virtual std::string name() = 0; // Desktop implementation is not const + virtual StorageReferenceInternal* GetParent() = 0; // Desktop implementation is not const + + virtual Future Delete() = 0; + virtual Future DeleteLastResult() = 0; + + virtual Future GetFile(const char* path, Listener* listener, + Controller* controller_out) = 0; + virtual Future GetFileLastResult() = 0; + + virtual Future GetBytes(void* buffer, size_t buffer_size, + Listener* listener, + Controller* controller_out) = 0; + virtual Future GetBytesLastResult() = 0; + + virtual Future GetDownloadUrl() = 0; + virtual Future GetDownloadUrlLastResult() = 0; + + virtual Future GetMetadata() = 0; + virtual Future GetMetadataLastResult() = 0; + + virtual Future UpdateMetadata(const Metadata* metadata) = 0; + virtual Future UpdateMetadataLastResult() = 0; + + virtual Future PutBytes(const void* buffer, size_t buffer_size, + Listener* listener, + Controller* controller_out) = 0; + virtual Future PutBytes(const void* buffer, size_t buffer_size, + const Metadata* metadata, Listener* listener, + Controller* controller_out) = 0; + virtual Future PutBytesLastResult() = 0; + + virtual Future PutFile(const char* path, Listener* listener, + Controller* controller_out) = 0; + virtual Future PutFile(const char* path, const Metadata* metadata, + Listener* listener, + Controller* controller_out) = 0; + virtual Future PutFileLastResult() = 0; + + virtual Future List(int32_t max_results) = 0; + virtual Future List(int32_t max_results, + const char* page_token) = 0; + virtual Future ListAll() = 0; + virtual Future ListLastResult() = 0; + + // Common utility methods needed by StorageReference and platform implementations + virtual StorageInternal* storage_internal() const = 0; + virtual StorageReferenceInternal* Clone() const = 0; + + protected: + StorageReferenceInternal() = default; + + private: + // Public class is a friend to access constructor & internal_ + friend class firebase::storage::StorageReference; + + // Disallow copy/move of the base class directly; cloning is explicit. + StorageReferenceInternal(const StorageReferenceInternal&) = delete; + StorageReferenceInternal& operator=(const StorageReferenceInternal&) = delete; + StorageReferenceInternal(StorageReferenceInternal&&) = delete; + StorageReferenceInternal& operator=(StorageReferenceInternal&&) = delete; +}; + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_COMMON_STORAGE_REFERENCE_INTERNAL_H_ diff --git a/storage/src/desktop/controller_desktop.cc b/storage/src/desktop/controller_desktop.cc index 64894dc414..98752a3dfa 100644 --- a/storage/src/desktop/controller_desktop.cc +++ b/storage/src/desktop/controller_desktop.cc @@ -82,8 +82,8 @@ int64_t ControllerInternal::total_byte_count() { // Returns the StorageReference associated with this Controller. StorageReferenceInternal* ControllerInternal::GetReference() const { MutexLock lock(mutex_); - return reference_.is_valid() - ? new StorageReferenceInternal(*reference_.internal_) + return reference_.is_valid() && reference_.internal_ != nullptr + ? reference_.internal_->Clone() : nullptr; } diff --git a/storage/src/desktop/list_result_desktop.cc b/storage/src/desktop/list_result_desktop.cc new file mode 100644 index 0000000000..44014b2c78 --- /dev/null +++ b/storage/src/desktop/list_result_desktop.cc @@ -0,0 +1,58 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "storage/src/desktop/list_result_desktop.h" + +#include "storage/src/desktop/storage_desktop.h" + +namespace firebase { +namespace storage { +namespace internal { + +ListResultInternal::ListResultInternal(StorageInternal* storage_internal) + : storage_internal_(storage_internal) {} + +ListResultInternal::ListResultInternal( + StorageInternal* storage_internal, + const std::vector& items, + const std::vector& prefixes, + const std::string& page_token) + : storage_internal_(storage_internal), + items_stub_(items), + prefixes_stub_(prefixes), + page_token_stub_(page_token) {} + +ListResultInternal::ListResultInternal(const ListResultInternal& other) + : storage_internal_(other.storage_internal_), + items_stub_(other.items_stub_), + prefixes_stub_(other.prefixes_stub_), + page_token_stub_(other.page_token_stub_) {} + +ListResultInternal& ListResultInternal::operator=( + const ListResultInternal& other) { + if (&other == this) { + return *this; + } + storage_internal_ = other.storage_internal_; + items_stub_ = other.items_stub_; + prefixes_stub_ = other.prefixes_stub_; + page_token_stub_ = other.page_token_stub_; + return *this; +} + +// Methods are already stubbed inline in the header. + +} // namespace internal +} // namespace storage +} // namespace firebase diff --git a/storage/src/desktop/list_result_desktop.h b/storage/src/desktop/list_result_desktop.h new file mode 100644 index 0000000000..56f53e4460 --- /dev/null +++ b/storage/src/desktop/list_result_desktop.h @@ -0,0 +1,83 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_DESKTOP_LIST_RESULT_DESKTOP_H_ +#define FIREBASE_STORAGE_SRC_DESKTOP_LIST_RESULT_DESKTOP_H_ + +#include +#include + +#include "firebase/storage/storage_reference.h" +#include "storage/src/desktop/storage_desktop.h" + +namespace firebase { +namespace storage { + +// Forward declaration for platform-specific ListResultInternal. +class ListResult; + +namespace internal { + +// Contains the Desktop-specific implementation of ListResultInternal (stubs). +class ListResultInternal { + public: + // Constructor. + explicit ListResultInternal(StorageInternal* storage_internal); + + // Constructor that can take pre-populated data (though stubs won't use it). + ListResultInternal(StorageInternal* storage_internal, + const std::vector& items, + const std::vector& prefixes, + const std::string& page_token); + + // Destructor (default is fine). + ~ListResultInternal() = default; + + // Copy constructor. + ListResultInternal(const ListResultInternal& other); + + // Copy assignment operator. + ListResultInternal& operator=(const ListResultInternal& other); + + // Gets the items (files) in this result (stub). + std::vector items() const { + return std::vector(); + } + + // Gets the prefixes (folders) in this result (stub). + std::vector prefixes() const { + return std::vector(); + } + + // Gets the page token for the next page of results (stub). + std::string page_token() const { return ""; } + + // Returns the StorageInternal object associated with this ListResult. + StorageInternal* storage_internal() const { return storage_internal_; } + + private: + friend class firebase::storage::ListResult; + + StorageInternal* storage_internal_; // Not owned. + // Desktop stubs don't actually store these, but defined to match constructor. + std::vector items_stub_; + std::vector prefixes_stub_; + std::string page_token_stub_; +}; + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_DESKTOP_LIST_RESULT_DESKTOP_H_ diff --git a/storage/src/desktop/metadata_desktop.cc b/storage/src/desktop/metadata_desktop.cc index d0d3fc86c1..e923b81459 100644 --- a/storage/src/desktop/metadata_desktop.cc +++ b/storage/src/desktop/metadata_desktop.cc @@ -144,7 +144,9 @@ void MetadataInternal::UpdateStorageInternal() { } StorageReferenceInternal* MetadataInternal::GetReference() const { - return new StorageReferenceInternal(*storage_reference_.internal_); + return storage_reference_.is_valid() && storage_reference_.internal_ != nullptr + ? storage_reference_.internal_->Clone() + : nullptr; } std::string MetadataInternal::LookUpString(Variant* root, const char* key, diff --git a/storage/src/desktop/storage_desktop.cc b/storage/src/desktop/storage_desktop.cc index 10ed0896b5..797dc4f258 100644 --- a/storage/src/desktop/storage_desktop.cc +++ b/storage/src/desktop/storage_desktop.cc @@ -77,20 +77,20 @@ StorageInternal::~StorageInternal() { // Get a StorageReference to the root of the database. StorageReferenceInternal* StorageInternal::GetReference() const { - return new StorageReferenceInternal(url_, const_cast(this)); + return new StorageReferenceInternalDesktop(url_, const_cast(this)); } // Get a StorageReference for the specified path. StorageReferenceInternal* StorageInternal::GetReference( const char* path) const { - return new StorageReferenceInternal(root_.GetChild(path), + return new StorageReferenceInternalDesktop(root_.GetChild(path), const_cast(this)); } // Get a StorageReference for the provided URL. StorageReferenceInternal* StorageInternal::GetReferenceFromUrl( const char* url) const { - return new StorageReferenceInternal(url, const_cast(this)); + return new StorageReferenceInternalDesktop(url, const_cast(this)); } // Returns the auth token for the current user, if there is a current user, diff --git a/storage/src/desktop/storage_desktop.h b/storage/src/desktop/storage_desktop.h index d20f544952..d91efa47aa 100644 --- a/storage/src/desktop/storage_desktop.h +++ b/storage/src/desktop/storage_desktop.h @@ -82,6 +82,9 @@ class StorageInternal { // Whether this object was successfully initialized by the constructor. bool initialized() const { return app_ != nullptr; } + // Conforms to common interface, equivalent to initialized() for desktop. + bool app_valid() const { return initialized(); } + // When this is deleted, it will clean up all StorageReferences and other // objects. FutureManager& future_manager() { return future_manager_; } diff --git a/storage/src/desktop/storage_reference_desktop.cc b/storage/src/desktop/storage_reference_desktop.cc index aa99863e3b..8ee2b9e83d 100644 --- a/storage/src/desktop/storage_reference_desktop.cc +++ b/storage/src/desktop/storage_reference_desktop.cc @@ -28,8 +28,10 @@ #include "app/src/function_registry.h" #include "app/src/include/firebase/app.h" #include "app/src/thread.h" +#include "firebase/storage/list_result.h" #include "storage/src/common/common_internal.h" #include "storage/src/desktop/controller_desktop.h" +#include "storage/src/desktop/list_result_desktop.h" #include "storage/src/desktop/metadata_desktop.h" #include "storage/src/desktop/storage_desktop.h" #include "storage/src/include/firebase/storage.h" @@ -46,59 +48,63 @@ namespace internal { * (see Google Cloud Storage) */ -StorageReferenceInternal::StorageReferenceInternal( +StorageReferenceInternalDesktop::StorageReferenceInternalDesktop( const std::string& storageUri, StorageInternal* storage) : storage_(storage), storageUri_(storageUri) { storage_->future_manager().AllocFutureApi(this, kStorageReferenceFnCount); } -StorageReferenceInternal::StorageReferenceInternal( +StorageReferenceInternalDesktop::StorageReferenceInternalDesktop( const StoragePath& storageUri, StorageInternal* storage) : storage_(storage), storageUri_(storageUri) { storage_->future_manager().AllocFutureApi(this, kStorageReferenceFnCount); } -StorageReferenceInternal::StorageReferenceInternal( - const StorageReferenceInternal& other) +StorageReferenceInternalDesktop::StorageReferenceInternalDesktop( + const StorageReferenceInternalDesktop& other) : storage_(other.storage_), storageUri_(other.storageUri_) { storage_->future_manager().AllocFutureApi(this, kStorageReferenceFnCount); } -StorageReferenceInternal::~StorageReferenceInternal() { +StorageReferenceInternalDesktop::~StorageReferenceInternalDesktop() { storage_->future_manager().ReleaseFutureApi(this); } +StorageReferenceInternal* StorageReferenceInternalDesktop::Clone() const { + return new StorageReferenceInternalDesktop(*this); +} + // Gets the storage to which we refer. -Storage* StorageReferenceInternal::storage() const { +Storage* StorageReferenceInternalDesktop::storage() const { return Storage::GetInstance(storage_->app()); } // Return the Google Cloud Storage bucket that holds this object. -std::string StorageReferenceInternal::bucket() const { +std::string StorageReferenceInternalDesktop::bucket() const { return storageUri_.GetBucket(); } // Return the full path of the object. -std::string StorageReferenceInternal::full_path() const { +std::string StorageReferenceInternalDesktop::full_path() const { return "/" + storageUri_.GetPath().str(); } // Gets a reference to a location relative to this one. -StorageReferenceInternal* StorageReferenceInternal::Child( +StorageReferenceInternal* StorageReferenceInternalDesktop::Child( const char* path) const { if (path == nullptr) return nullptr; - return new StorageReferenceInternal(storageUri_.GetChild(path), storage_); + return new StorageReferenceInternalDesktop(storageUri_.GetChild(path), storage_); } -StorageReference StorageReferenceInternal::AsStorageReference() const { - return StorageReference(new StorageReferenceInternal(*this)); +StorageReference StorageReferenceInternalDesktop::AsStorageReference() const { + return StorageReference(new StorageReferenceInternalDesktop(*this)); } // Handy utility function. Takes ownership of request/response controllers // passed in, and will delete them when the request is complete. // (listener and controller_out are not deleted, since they are owned by the // calling function, if they exist.) -void StorageReferenceInternal::RestCall(rest::Request* request, +void StorageReferenceInternalDesktop::RestCall(rest::Request* request, Notifier* request_notifier, BlockingResponse* response, FutureHandle handle, Listener* listener, @@ -154,9 +160,9 @@ struct MetadataChainData { // Basically just chains futures together via OnCompletion callbacks, but // has a few tricky bits, where it uses a copy of the original reference // to hide the internal futures from the user, so the user can just deal with -// one external future. (Which is completed by the OnCompletion chain when -// all of the operations have concluded.) -void StorageReferenceInternal::SetupMetadataChain( +// (listener and controller_out are not deleted, since they are owned by the +// calling function, if they exist.) +void StorageReferenceInternalDesktop::SetupMetadataChain( Future starting_future, MetadataChainData* data) { data->inner_future = starting_future; starting_future.OnCompletion( @@ -201,7 +207,7 @@ void StorageReferenceInternal::SetupMetadataChain( } // Deletes the object at the current path. -Future StorageReferenceInternal::Delete() { +Future StorageReferenceInternalDesktop::Delete() { auto* future_api = future(); auto handle = future_api->SafeAlloc(kStorageReferenceFnDelete); @@ -222,7 +228,7 @@ Future StorageReferenceInternal::Delete() { return DeleteLastResult(); } -Future StorageReferenceInternal::DeleteLastResult() { +Future StorageReferenceInternalDesktop::DeleteLastResult() { return static_cast&>( future()->LastResult(kStorageReferenceFnDelete)); } @@ -232,7 +238,7 @@ const int kAppCheckTokenTimeoutMs = 10000; // Handy utility function, since REST calls have similar setup and teardown. // Can potentially block getting Future results for the Request. -void StorageReferenceInternal::PrepareRequestBlocking( +void StorageReferenceInternalDesktop::PrepareRequestBlocking( rest::Request* request, const char* url, const char* method, const char* content_type) { request->set_url(url); @@ -273,7 +279,7 @@ void StorageReferenceInternal::PrepareRequestBlocking( } // Asynchronously downloads the object from this StorageReference. -Future StorageReferenceInternal::GetFile(const char* path, +Future StorageReferenceInternalDesktop::GetFile(const char* path, Listener* listener, Controller* controller_out) { auto handle = future()->SafeAlloc(kStorageReferenceFnGetFile); @@ -298,13 +304,13 @@ Future StorageReferenceInternal::GetFile(const char* path, return GetFileLastResult(); } -Future StorageReferenceInternal::GetFileLastResult() { +Future StorageReferenceInternalDesktop::GetFileLastResult() { return static_cast&>( future()->LastResult(kStorageReferenceFnGetFile)); } // Asynchronously downloads the object from this StorageReference. -Future StorageReferenceInternal::GetBytes(void* buffer, +Future StorageReferenceInternalDesktop::GetBytes(void* buffer, size_t buffer_size, Listener* listener, Controller* controller_out) { @@ -330,13 +336,13 @@ Future StorageReferenceInternal::GetBytes(void* buffer, // Sends a rest request, and creates a separate thread to retry failures. template -void StorageReferenceInternal::SendRequestWithRetry( +void StorageReferenceInternalDesktop::SendRequestWithRetry( StorageReferenceFn internal_function_reference, SendRequestFunct send_request_funct, SafeFutureHandle final_handle, double max_retry_time_seconds) { BlockingResponse* first_response = send_request_funct(); std::thread async_retry_thread( - &StorageReferenceInternal::AsyncSendRequestWithRetry, this, + &StorageReferenceInternalDesktop::AsyncSendRequestWithRetry, this, internal_function_reference, send_request_funct, final_handle, first_response, max_retry_time_seconds); async_retry_thread.detach(); @@ -348,7 +354,7 @@ const int kMaxSleepTimeMillis = 30000; // In a separate thread, repeatedly send Rest requests until one succeeds or a // maximum amount of time has passed. template -void StorageReferenceInternal::AsyncSendRequestWithRetry( +void StorageReferenceInternalDesktop::AsyncSendRequestWithRetry( StorageReferenceFn internal_function_reference, SendRequestFunct send_request_funct, SafeFutureHandle final_handle, BlockingResponse* response, @@ -406,7 +412,7 @@ bool g_retry_all_errors_for_testing = false; // Returns whether or not an http status represents a failure that should be // retried. -bool StorageReferenceInternal::IsRetryableFailure(int httpStatus) { +bool StorageReferenceInternalDesktop::IsRetryableFailure(int httpStatus) { return (httpStatus >= 500 && httpStatus < 600) || httpStatus == 429 || httpStatus == 408 || (g_retry_all_errors_for_testing && @@ -414,20 +420,20 @@ bool StorageReferenceInternal::IsRetryableFailure(int httpStatus) { } // Returns the result of the most recent call to GetBytes(); -Future StorageReferenceInternal::GetBytesLastResult() { +Future StorageReferenceInternalDesktop::GetBytesLastResult() { return static_cast&>( future()->LastResult(kStorageReferenceFnGetBytes)); } // Asynchronously uploads data to the currently specified StorageReference, // without additional metadata. -Future StorageReferenceInternal::PutBytes( +Future StorageReferenceInternalDesktop::PutBytes( const void* buffer, size_t buffer_size, Listener* listener, Controller* controller_out) { return PutBytes(buffer, buffer_size, nullptr, listener, controller_out); } -Future StorageReferenceInternal::PutBytesInternal( +Future StorageReferenceInternalDesktop::PutBytesInternal( const void* buffer, size_t buffer_size, Listener* listener, Controller* controller_out, const char* content_type) { auto* future_api = future(); @@ -458,7 +464,7 @@ Future StorageReferenceInternal::PutBytesInternal( // Asynchronously uploads data to the currently specified StorageReference, // with metadata included. -Future StorageReferenceInternal::PutBytes( +Future StorageReferenceInternalDesktop::PutBytes( const void* buffer, size_t buffer_size, const Metadata* metadata, Listener* listener, Controller* controller_out) { // This is the handle for the actual future returned to the user. @@ -469,8 +475,11 @@ Future StorageReferenceInternal::PutBytes( // This is the future to do the actual putfile. Note that it is on a // different storage reference than the original, so the caller of this // function can't access it via PutFileLastResult. + // Cast to Desktop specific internal to call the private helper. + StorageReferenceInternalDesktop* desktop_internal_for_put = + static_cast(data->storage_ref.internal_); Future putbytes_internal = - data->storage_ref.internal_->PutBytesInternal( + desktop_internal_for_put->PutBytesInternal( buffer, buffer_size, listener, controller_out, metadata ? metadata->content_type() : nullptr); @@ -479,7 +488,7 @@ Future StorageReferenceInternal::PutBytes( return PutBytesLastResult(); } -Future StorageReferenceInternal::PutBytesLastResult() { +Future StorageReferenceInternalDesktop::PutBytesLastResult() { return static_cast&>( future()->LastResult(kStorageReferenceFnPutBytes)); } @@ -488,13 +497,13 @@ Future StorageReferenceInternal::PutBytesLastResult() { // without additional metadata. // Currently not terribly efficient about it. // TODO(b/69434445): Make this more efficient. b/69434445 -Future StorageReferenceInternal::PutFile(const char* path, +Future StorageReferenceInternalDesktop::PutFile(const char* path, Listener* listener, Controller* controller_out) { return PutFile(path, nullptr, listener, controller_out); } -Future StorageReferenceInternal::PutFileInternal( +Future StorageReferenceInternalDesktop::PutFileInternal( const char* path, Listener* listener, Controller* controller_out, const char* content_type) { auto* future_api = future(); @@ -534,7 +543,7 @@ Future StorageReferenceInternal::PutFileInternal( // Asynchronously uploads data to the currently specified StorageReference, // without additional metadata. -Future StorageReferenceInternal::PutFile(const char* path, +Future StorageReferenceInternalDesktop::PutFile(const char* path, const Metadata* metadata, Listener* listener, Controller* controller_out) { @@ -546,8 +555,11 @@ Future StorageReferenceInternal::PutFile(const char* path, // This is the future to do the actual putfile. Note that it is on a // different storage reference than the original, so the caller of this // function can't access it via PutFileLastResult. + // Cast to Desktop specific internal to call the private helper. + StorageReferenceInternalDesktop* desktop_internal_for_put = + static_cast(data->storage_ref.internal_); Future putfile_internal = - data->storage_ref.internal_->PutFileInternal( + desktop_internal_for_put->PutFileInternal( path, listener, controller_out, metadata ? metadata->content_type() : nullptr); @@ -557,13 +569,13 @@ Future StorageReferenceInternal::PutFile(const char* path, } // Returns the result of the most recent call to PutFile(); -Future StorageReferenceInternal::PutFileLastResult() { +Future StorageReferenceInternalDesktop::PutFileLastResult() { return static_cast&>( future()->LastResult(kStorageReferenceFnPutFile)); } // Retrieves metadata associated with an object at this StorageReference. -Future StorageReferenceInternal::GetMetadata() { +Future StorageReferenceInternalDesktop::GetMetadata() { auto* future_api = future(); auto handle = future_api->SafeAlloc(kStorageReferenceFnGetMetadata); @@ -590,13 +602,13 @@ Future StorageReferenceInternal::GetMetadata() { } // Returns the result of the most recent call to GetMetadata(); -Future StorageReferenceInternal::GetMetadataLastResult() { +Future StorageReferenceInternalDesktop::GetMetadataLastResult() { return static_cast&>( future()->LastResult(kStorageReferenceFnGetMetadata)); } // Updates the metadata associated with this StorageReference. -Future StorageReferenceInternal::UpdateMetadata( +Future StorageReferenceInternalDesktop::UpdateMetadata( const Metadata* metadata) { auto* future_api = future(); auto handle = @@ -629,13 +641,13 @@ Future StorageReferenceInternal::UpdateMetadata( } // Returns the result of the most recent call to UpdateMetadata(); -Future StorageReferenceInternal::UpdateMetadataLastResult() { +Future StorageReferenceInternalDesktop::UpdateMetadataLastResult() { return static_cast&>( future()->LastResult(kStorageReferenceFnUpdateMetadata)); } // Asynchronously retrieves a long lived download URL with a revokable token. -Future StorageReferenceInternal::GetDownloadUrl() { +Future StorageReferenceInternalDesktop::GetDownloadUrl() { // TODO(b/78908154): Re-implement this function without use of GetMetadata() Future metadata_future = GetMetadata(); auto* future_api = future(); @@ -678,26 +690,62 @@ Future StorageReferenceInternal::GetDownloadUrl() { } // Returns the result of the most recent call to GetDownloadUrl(); -Future StorageReferenceInternal::GetDownloadUrlLastResult() { +Future StorageReferenceInternalDesktop::GetDownloadUrlLastResult() { return static_cast&>( future()->LastResult(kStorageReferenceFnGetDownloadUrl)); } // Returns the short name of this object. -std::string StorageReferenceInternal::name() { +std::string StorageReferenceInternalDesktop::name() { return storageUri_.GetPath().GetBaseName(); } // Returns a new instance of StorageReference pointing to the parent location // or null if this instance references the root location. -StorageReferenceInternal* StorageReferenceInternal::GetParent() { - return new StorageReferenceInternal(storageUri_.GetParent(), storage_); +StorageReferenceInternal* StorageReferenceInternalDesktop::GetParent() { + return new StorageReferenceInternalDesktop(storageUri_.GetParent(), storage_); } -ReferenceCountedFutureImpl* StorageReferenceInternal::future() { +ReferenceCountedFutureImpl* StorageReferenceInternalDesktop::future() { return storage_->future_manager().GetFutureApi(this); } +Future StorageReferenceInternalDesktop::List(int32_t max_results) { + ReferenceCountedFutureImpl* future_api = future(); + SafeFutureHandle handle = + future_api->SafeAlloc(kStorageReferenceFnList); + future_api->CompleteWithResult(handle, firebase::storage::kErrorUnimplemented, + "List operation is not supported on desktop.", + ListResult(nullptr)); + return ListLastResult(); +} + +Future StorageReferenceInternalDesktop::List(int32_t max_results, + const char* page_token) { + ReferenceCountedFutureImpl* future_api = future(); + SafeFutureHandle handle = + future_api->SafeAlloc(kStorageReferenceFnList); + future_api->CompleteWithResult(handle, firebase::storage::kErrorUnimplemented, + "List operation is not supported on desktop.", + ListResult(nullptr)); + return ListLastResult(); +} + +Future StorageReferenceInternalDesktop::ListAll() { + ReferenceCountedFutureImpl* future_api = future(); + SafeFutureHandle handle = + future_api->SafeAlloc(kStorageReferenceFnList); + future_api->CompleteWithResult( + handle, firebase::storage::kErrorUnimplemented, + "ListAll operation is not supported on desktop.", ListResult(nullptr)); + return ListLastResult(); +} + +Future StorageReferenceInternalDesktop::ListLastResult() { + return static_cast&>( + future()->LastResult(kStorageReferenceFnList)); +} + } // namespace internal } // namespace storage } // namespace firebase diff --git a/storage/src/desktop/storage_reference_desktop.h b/storage/src/desktop/storage_reference_desktop.h index bbda95d342..b1fe3eaf06 100644 --- a/storage/src/desktop/storage_reference_desktop.h +++ b/storage/src/desktop/storage_reference_desktop.h @@ -23,6 +23,7 @@ #include "storage/src/desktop/curl_requests.h" #include "storage/src/desktop/storage_path.h" #include "storage/src/include/firebase/storage/storage_reference.h" +#include "storage/src/common/storage_reference_internal.h" // Include the base class definition namespace firebase { namespace storage { @@ -44,6 +45,7 @@ enum StorageReferenceFn { kStorageReferenceFnPutBytesInternal, kStorageReferenceFnPutFile, kStorageReferenceFnPutFileInternal, + kStorageReferenceFnList, // Added for List operations kStorageReferenceFnCount, }; @@ -51,102 +53,113 @@ class BlockingResponse; class MetadataChainData; class Notifier; -class StorageReferenceInternal { +class StorageReferenceInternalDesktop : public StorageReferenceInternal { public: - StorageReferenceInternal(const std::string& storageUri, - StorageInternal* storage); - StorageReferenceInternal(const StoragePath& storageUri, - StorageInternal* storage); - StorageReferenceInternal(const StorageReferenceInternal& other); + StorageReferenceInternalDesktop(const std::string& storageUri, + StorageInternal* storage); + StorageReferenceInternalDesktop(const StoragePath& storageUri, + StorageInternal* storage); + StorageReferenceInternalDesktop(const StorageReferenceInternalDesktop& other); - ~StorageReferenceInternal(); + ~StorageReferenceInternalDesktop() override; + + // Implementations for pure virtuals from StorageReferenceInternal + StorageInternal* storage_internal() const override { return storage_; } + StorageReferenceInternal* Clone() const override; // Gets the storage to which we refer. - Storage* storage() const; + Storage* storage() const override; // Gets a reference to a location relative to this one. - StorageReferenceInternal* Child(const char* path) const; + StorageReferenceInternal* Child(const char* path) const override; // Return the Google Cloud Storage bucket that holds this object. - std::string bucket() const; + std::string bucket() const override; // Return the full path of the object. - std::string full_path() const; + std::string full_path() const override; // Returns the short name of this object. - std::string name(); + std::string name() override; // Returns a new instance of StorageReference pointing to the parent location // or null if this instance references the root location. - StorageReferenceInternal* GetParent(); + StorageReferenceInternal* GetParent() override; // Deletes the object at the current path. - Future Delete(); + Future Delete() override; // Returns the result of the most recent call to Delete(); - Future DeleteLastResult(); + Future DeleteLastResult() override; // Asynchronously downloads the object from this StorageReference. Future GetFile(const char* path, Listener* listener, - Controller* controller_out); + Controller* controller_out) override; // Returns the result of the most recent call to GetFile(); - Future GetFileLastResult(); + Future GetFileLastResult() override; // Asynchronously downloads the object from this StorageReference. Future GetBytes(void* buffer, size_t buffer_size, Listener* listener, - Controller* controller_out); + Controller* controller_out) override; // Returns the result of the most recent call to GetBytes(); - Future GetBytesLastResult(); + Future GetBytesLastResult() override; // Asynchronously retrieves a long lived download URL with a revokable token. - Future GetDownloadUrl(); + Future GetDownloadUrl() override; // Returns the result of the most recent call to GetDownloadUrl(); - Future GetDownloadUrlLastResult(); + Future GetDownloadUrlLastResult() override; // Retrieves metadata associated with an object at this StorageReference. - Future GetMetadata(); + Future GetMetadata() override; // Returns the result of the most recent call to GetMetadata(); - Future GetMetadataLastResult(); + Future GetMetadataLastResult() override; // Updates the metadata associated with this StorageReference. - Future UpdateMetadata(const Metadata* metadata); + Future UpdateMetadata(const Metadata* metadata) override; // Returns the result of the most recent call to UpdateMetadata(); - Future UpdateMetadataLastResult(); + Future UpdateMetadataLastResult() override; // Asynchronously uploads data to the currently specified StorageReference, // without additional metadata. Future PutBytes(const void* buffer, size_t buffer_size, - Listener* listener, Controller* controller_out); + Listener* listener, Controller* controller_out) override; // Asynchronously uploads data to the currently specified StorageReference, // without additional metadata. Future PutBytes(const void* buffer, size_t buffer_size, const Metadata* metadata, Listener* listener, - Controller* controller_out); + Controller* controller_out) override; // Returns the result of the most recent call to Write(); - Future PutBytesLastResult(); + Future PutBytesLastResult() override; // Asynchronously uploads data to the currently specified StorageReference, // without additional metadata. Future PutFile(const char* path, Listener* listener, - Controller* controller_out); + Controller* controller_out) override; // Asynchronously uploads data to the currently specified StorageReference, // without additional metadata. Future PutFile(const char* path, const Metadata* metadata, - Listener* listener, Controller* controller_out); + Listener* listener, Controller* controller_out) override; // Returns the result of the most recent call to Write(); - Future PutFileLastResult(); + Future PutFileLastResult() override; + + // Asynchronously lists objects and common prefixes under this reference + Future List(int32_t max_results) override; + Future List(int32_t max_results, const char* page_token) override; + + // Asynchronously lists all objects and common prefixes under this reference + Future ListAll() override; - // Pointer to the StorageInternal instance we are a part of. - StorageInternal* storage_internal() const { return storage_; } + // Returns the result of the most recent List operation + Future ListLastResult() override; // Wrap this in a StorageReference. // Exposed for testing. diff --git a/storage/src/include/firebase/storage/common.h b/storage/src/include/firebase/storage/common.h index 567ed71492..477f125c72 100644 --- a/storage/src/include/firebase/storage/common.h +++ b/storage/src/include/firebase/storage/common.h @@ -47,6 +47,8 @@ enum Error { kErrorDownloadSizeExceeded, /// User cancelled the operation. kErrorCancelled, + /// The operation is not implemented on the current platform. + kErrorUnimplemented, }; /// @brief Get the human-readable error message corresponding to an error code. diff --git a/storage/src/include/firebase/storage/future_details.h b/storage/src/include/firebase/storage/future_details.h new file mode 100644 index 0000000000..9a82e92bfa --- /dev/null +++ b/storage/src/include/firebase/storage/future_details.h @@ -0,0 +1,21 @@ +#ifndef FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_FUTURE_DETAILS_H_ +#define FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_FUTURE_DETAILS_H_ + +// This header is currently missing from the repository. +// Providing a minimal placeholder to allow compilation to proceed. +// It likely contains helper types or functions for Storage Futures. + +namespace firebase { +namespace storage { +namespace internal { + +// Example: If StorageReferenceFn (or other enums/structs) were used +// by future management logic that might have been in this file. +// For now, keeping it empty until specific needs are identified by +// compiler errors from storage_reference.cc. + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_FUTURE_DETAILS_H_ diff --git a/storage/src/include/firebase/storage/list_result.h b/storage/src/include/firebase/storage/list_result.h new file mode 100644 index 0000000000..66a06ddb6f --- /dev/null +++ b/storage/src/include/firebase/storage/list_result.h @@ -0,0 +1,89 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_LIST_RESULT_H_ +#define FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_LIST_RESULT_H_ + +#include +#include + +#include "firebase/storage/storage_reference.h" + +namespace firebase { +namespace storage { + +// Forward declaration for internal class. +namespace internal { +class ListResultInternal; +} // namespace internal + +/// @brief ListResult contains a list of items and prefixes from a list() +/// call. +/// +/// This is a result from a list() call on a StorageReference. +class ListResult { + public: + /// @brief Creates an invalid ListResult. + ListResult(); + + /// @brief Creates a ListResult from an internal ListResult object. + /// + /// This constructor is not intended for public use. + explicit ListResult(internal::ListResultInternal* internal); + + /// @brief Copy constructor. + ListResult(const ListResult& other); + + /// @brief Copy assignment operator. + ListResult& operator=(const ListResult& other); + + /// @brief Move constructor. + ListResult(ListResult&& other); + + /// @brief Move assignment operator. + ListResult& operator=(ListResult&& other); + + /// @brief Destructor. + ~ListResult(); + + /// @brief Returns the items (files) in this result. + std::vector items() const; + + /// @brief Returns the prefixes (folders) in this result. + std::vector prefixes() const; + + /// @brief If set, there are more results to retrieve. + /// + /// Pass this token to list() to retrieve the next page of results. + std::string page_token() const; + + /// @brief Returns true if this ListResult is valid, false if it is not + /// valid. An invalid ListResult indicates that the operation that was + /// to create this ListResult failed. + bool is_valid() const; + + private: + /// @cond FIREBASE_APP_INTERNAL + // Method called by the CleanupNotifier global callback to perform cleanup. + void ClearInternalForCleanup(); + friend void GlobalCleanupListResult(void* list_result_void); + /// @endcond + + internal::ListResultInternal* internal_; +}; + +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_INCLUDE_FIREBASE_STORAGE_LIST_RESULT_H_ diff --git a/storage/src/include/firebase/storage/metadata.h b/storage/src/include/firebase/storage/metadata.h index 8d697c1072..ee544917ea 100644 --- a/storage/src/include/firebase/storage/metadata.h +++ b/storage/src/include/firebase/storage/metadata.h @@ -30,6 +30,7 @@ class MetadataInternal; class MetadataInternalCommon; class StorageInternal; class StorageReferenceInternal; +class StorageReferenceInternalDesktop; // Forward declaration } // namespace internal class Storage; @@ -261,6 +262,7 @@ class Metadata { friend class internal::MetadataInternal; friend class internal::MetadataInternalCommon; friend class internal::StorageReferenceInternal; + friend class internal::StorageReferenceInternalDesktop; // Added friendship #ifndef INTERNAL_EXPERIMENTAL Metadata(internal::MetadataInternal* internal); diff --git a/storage/src/include/firebase/storage/storage_reference.h b/storage/src/include/firebase/storage/storage_reference.h index e5c7c2f85a..73943344f0 100644 --- a/storage/src/include/firebase/storage/storage_reference.h +++ b/storage/src/include/firebase/storage/storage_reference.h @@ -25,9 +25,11 @@ namespace firebase { namespace storage { +// Forward declarations class Controller; class Listener; class Storage; +class ListResult; /// @cond FIREBASE_APP_INTERNAL namespace internal { @@ -339,6 +341,60 @@ class StorageReference { /// StorageReference is invalid. bool is_valid() const; + /// @brief Asynchronously lists objects and common prefixes under this + /// StorageReference. + /// + /// This method allows you to list objects and common prefixes (virtual + /// subdirectories) directly under this StorageReference. + /// + /// @param[in] max_results The maximum number of items and prefixes to return + /// in a single page. Must be greater than 0 and at most 1000. + /// + /// @return A Future that will eventually contain a ListResult. + /// If the operation is successful, the ListResult will contain the first + /// page of items and prefixes, and potentially a page_token to retrieve + /// subsequent pages. + Future List(int32_t max_results); + + /// @brief Asynchronously lists objects and common prefixes under this + /// StorageReference. + /// + /// This method allows you to list objects and common prefixes (virtual + /// subdirectories) directly under this StorageReference. + /// + /// @param[in] max_results The maximum number of items and prefixes to return + /// in a single page. Must be greater than 0 and at most 1000. + /// @param[in] page_token A page token, returned from a previous call to + /// List, to retrieve the next page of results. If nullptr or an empty + /// string, retrieves the first page. + /// + /// @return A Future that will eventually contain a ListResult. + /// If the operation is successful, the ListResult will contain the + /// requested page of items and prefixes, and potentially a page_token + /// to retrieve subsequent pages. + Future List(int32_t max_results, const char* page_token); + + /// @brief Asynchronously lists all objects and common prefixes under this + /// StorageReference. + /// + /// This method will list all items and prefixes under the current reference + /// by making multiple calls to the backend service if necessary, until all + /// results have been fetched. + /// + /// @note This can be a long-running and memory-intensive operation if there + /// are many objects under the reference. Consider using the paginated + /// List() method for very large directories. + /// + /// @return A Future that will eventually contain a ListResult. + /// If the operation is successful, the ListResult will contain all items + /// and prefixes. The page_token in the result will be empty. + Future ListAll(); + + /// @brief Returns the result of the most recent call to List() or ListAll(). + /// + /// @return The result of the most recent call to List() or ListAll(). + Future ListLastResult(); + private: /// @cond FIREBASE_APP_INTERNAL friend class Controller; @@ -348,6 +404,7 @@ class StorageReference { friend class Storage; friend class internal::StorageReferenceInternal; friend class internal::StorageReferenceInternalCommon; + friend class internal::StorageReferenceInternalDesktop; // Add friend for desktop internal class StorageReference(internal::StorageReferenceInternal* internal); diff --git a/storage/src/ios/fir_storage_list_result_pointer.h b/storage/src/ios/fir_storage_list_result_pointer.h new file mode 100644 index 0000000000..03e6dfeb01 --- /dev/null +++ b/storage/src/ios/fir_storage_list_result_pointer.h @@ -0,0 +1,40 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_IOS_FIR_STORAGE_LIST_RESULT_POINTER_H_ +#define FIREBASE_STORAGE_SRC_IOS_FIR_STORAGE_LIST_RESULT_POINTER_H_ + +#include "app/src/ios/pointer_ios.h" + +// Forward declare Obj-C types +#ifdef __OBJC__ +@class FIRStorageListResult; +#else +typedef struct objc_object FIRStorageListResult; +#endif + +namespace firebase { +namespace storage { +namespace internal { + +// Define FIRStorageListResultPointer. This is an iOS specific implementation +// detail that is not exposed in the public API. +FIREBASE_DEFINE_POINTER_WRAPPER(FIRStorageListResultPointer, + FIRStorageListResult); + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_IOS_FIR_STORAGE_LIST_RESULT_POINTER_H_ diff --git a/storage/src/ios/list_result_ios.h b/storage/src/ios/list_result_ios.h new file mode 100644 index 0000000000..8bee94b523 --- /dev/null +++ b/storage/src/ios/list_result_ios.h @@ -0,0 +1,94 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef FIREBASE_STORAGE_SRC_IOS_LIST_RESULT_IOS_H_ +#define FIREBASE_STORAGE_SRC_IOS_LIST_RESULT_IOS_H_ + +#include +#include +#include + +#include "firebase/storage/storage_reference.h" +#include "storage/src/ios/storage_ios.h" +#include "storage/src/ios/fir_storage_list_result_pointer.h" + +// Forward declare Obj-C types +#ifdef __OBJC__ +@class FIRStorageListResult; +@class FIRStorageReference; +#else +typedef struct objc_object FIRStorageListResult; +typedef struct objc_object FIRStorageReference; +#endif + + +namespace firebase { +namespace storage { + +// Forward declaration for platform-specific ListResultInternal. +class ListResult; + +namespace internal { + +// Contains the iOS-specific implementation of ListResultInternal. +class ListResultInternal { + public: + // Constructor. + // Takes ownership of the impl unique_ptr. + ListResultInternal(StorageInternal* storage_internal, + std::unique_ptr impl); + + // Copy constructor. + ListResultInternal(const ListResultInternal& other); + + // Copy assignment operator. + ListResultInternal& operator=(const ListResultInternal& other); + + // Destructor (default is fine thanks to unique_ptr). + ~ListResultInternal() = default; + + // Gets the items (files) in this result. + std::vector items() const; + + // Gets the prefixes (folders) in this result. + std::vector prefixes() const; + + // Gets the page token for the next page of results. + // Returns an empty string if there are no more results. + std::string page_token() const; + + // Returns the underlying Objective-C FIRStorageListResult object. + FIRStorageListResult* impl() const { return impl_->get(); } + + // Returns the StorageInternal object associated with this ListResult. + StorageInternal* storage_internal() const { return storage_internal_; } + + private: + friend class firebase::storage::ListResult; + + // Converts an NSArray of FIRStorageReference objects to a C++ vector of + // C++ StorageReference objects. + std::vector ProcessObjectiveCReferenceArray( + NSArray* ns_array_ref) const; + + StorageInternal* storage_internal_; // Not owned. + // Pointer to the Objective-C FIRStorageListResult instance. + std::unique_ptr impl_; +}; + +} // namespace internal +} // namespace storage +} // namespace firebase + +#endif // FIREBASE_STORAGE_SRC_IOS_LIST_RESULT_IOS_H_ diff --git a/storage/src/ios/list_result_ios.mm b/storage/src/ios/list_result_ios.mm new file mode 100644 index 0000000000..a4fdb73b8e --- /dev/null +++ b/storage/src/ios/list_result_ios.mm @@ -0,0 +1,123 @@ +// Copyright 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "storage/src/ios/list_result_ios.h" + +#import +#import +#import + +#include "app/src/assert.h" +#include "app/src/ios/c_string_manager.h" +#include "storage/src/ios/converter_ios.h" +#include "storage/src/ios/storage_ios.h" +#include "storage/src/ios/storage_reference_ios.h" + +namespace firebase { +namespace storage { +namespace internal { + +ListResultInternal::ListResultInternal( + StorageInternal* storage_internal, + std::unique_ptr impl) + : storage_internal_(storage_internal), impl_(std::move(impl)) { + FIREBASE_ASSERT(storage_internal_ != nullptr); + FIREBASE_ASSERT(impl_ != nullptr && impl_->get() != nullptr); +} + +ListResultInternal::ListResultInternal(const ListResultInternal& other) + : storage_internal_(other.storage_internal_), impl_(nullptr) { + FIREBASE_ASSERT(storage_internal_ != nullptr); + if (other.impl_ && other.impl_->get()) { + // FIRStorageListResult does not conform to NSCopying. + // To "copy" it, we'd typically re-fetch or if it's guaranteed immutable, + // we could retain the original. However, unique_ptr implies single ownership. + // For now, this copy constructor will create a ListResultInternal that + // shares the *same* underlying Objective-C object by retaining it and + // creating a new FIRStorageListResultPointer. + // This is generally not safe if the object is mutable or if true deep copy semantics + // are expected by the C++ ListResult's copy constructor. + // Given ListResult is usually a snapshot, sharing might be acceptable. + // TODO(b/180010117): Clarify copy semantics for ListResultInternal on iOS. + // A truly safe copy would involve creating a new FIRStorageListResult with the same contents. + // For now, we are making the unique_ptr point to the same ObjC object. + // This is done by getting the raw pointer, creating a new unique_ptr that points to it, + // and relying on FIRStorageListResultPointer's constructor to retain it. + // This breaks unique_ptr's unique ownership if the original unique_ptr still exists and manages it. + // A better approach for copy would be to create a new FIRStorageListResult with the same properties. + // As a placeholder, we will make a "copy" that points to the same Obj-C object, + // which is what FIRStorageListResultPointer(other.impl_->get()) would do. + impl_ = std::make_unique(other.impl_->get()); + } +} + +ListResultInternal& ListResultInternal::operator=( + const ListResultInternal& other) { + if (&other == this) { + return *this; + } + storage_internal_ = other.storage_internal_; + FIREBASE_ASSERT(storage_internal_ != nullptr); + if (other.impl_ && other.impl_->get()) { + // See notes in copy constructor regarding shared ownership. + impl_ = std::make_unique(other.impl_->get()); + } else { + impl_.reset(); + } + return *this; +} + +std::vector +ListResultInternal::ProcessObjectiveCReferenceArray( + NSArray* ns_array_ref) const { + std::vector cpp_references; + if (ns_array_ref == nil) { + return cpp_references; + } + for (FIRStorageReference* objc_ref in ns_array_ref) { + FIREBASE_ASSERT(objc_ref != nil); + // The StorageReferenceInternal constructor takes ownership of the pointer if unique_ptr is used directly. + // Here, FIRStorageReferencePointer constructor will retain the objc_ref. + auto sfr_internal = new StorageReferenceInternal( + storage_internal_, + std::make_unique(objc_ref)); + cpp_references.push_back(StorageReference(sfr_internal)); + } + return cpp_references; +} + +std::vector ListResultInternal::items() const { + FIREBASE_ASSERT(impl_ != nullptr && impl_->get() != nullptr); + FIRStorageListResult* list_result_objc = impl_->get(); + return ProcessObjectiveCReferenceArray(list_result_objc.items); +} + +std::vector ListResultInternal::prefixes() const { + FIREBASE_ASSERT(impl_ != nullptr && impl_->get() != nullptr); + FIRStorageListResult* list_result_objc = impl_->get(); + return ProcessObjectiveCReferenceArray(list_result_objc.prefixes); +} + +std::string ListResultInternal::page_token() const { + FIREBASE_ASSERT(impl_ != nullptr && impl_->get() != nullptr); + FIRStorageListResult* list_result_objc = impl_->get(); + if (list_result_objc.pageToken == nil) { + return ""; + } + return NSStringToStdString(list_result_objc.pageToken); +} + +} // namespace internal +} // namespace storage +} // namespace firebase diff --git a/storage/src/ios/storage_reference_ios.h b/storage/src/ios/storage_reference_ios.h index c8b39cd54c..eea4abbe3d 100644 --- a/storage/src/ios/storage_reference_ios.h +++ b/storage/src/ios/storage_reference_ios.h @@ -160,6 +160,16 @@ class StorageReferenceInternal { // Returns the result of the most recent call to PutFile(); Future PutFileLastResult(); + // Asynchronously lists objects and common prefixes under this reference. + Future List(int32_t max_results); + Future List(int32_t max_results, const char* _Nullable page_token); + + // Asynchronously lists all objects and common prefixes under this reference. + Future ListAll(); + + // Returns the result of the most recent List operation. + Future ListLastResult(); + // StorageInternal instance we are associated with. StorageInternal* _Nullable storage_internal() const { return storage_; } diff --git a/storage/src/ios/storage_reference_ios.mm b/storage/src/ios/storage_reference_ios.mm index d2260e3416..9571132050 100644 --- a/storage/src/ios/storage_reference_ios.mm +++ b/storage/src/ios/storage_reference_ios.mm @@ -32,7 +32,6 @@ namespace storage { namespace internal { -// Should reads and writes share thier futures? enum StorageReferenceFn { kStorageReferenceFnDelete = 0, kStorageReferenceFnGetBytes, @@ -42,6 +41,7 @@ kStorageReferenceFnUpdateMetadata, kStorageReferenceFnPutBytes, kStorageReferenceFnPutFile, + kStorageReferenceFnList, kStorageReferenceFnCount, }; @@ -438,6 +438,93 @@ return static_cast&>(future()->LastResult(kStorageReferenceFnPutFile)); } +Future StorageReferenceInternal::List(int32_t max_results) { + ReferenceCountedFutureImpl* future_impl = future(); + SafeFutureHandle handle = + future_impl->SafeAlloc(kStorageReferenceFnList); + StorageInternal* storage_internal = storage_; + + FIRStorageVoidListResultError completion_block = + ^(FIRStorageListResult* _Nullable list_result_objc, NSError* _Nullable error) { + Error error_code = NSErrorToErrorCode(error); + const char* error_message = GetErrorMessage(error_code); + if (list_result_objc != nil) { + auto list_internal = new ListResultInternal( + storage_internal, + std::make_unique(list_result_objc)); + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(list_internal)); + } else { + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(nullptr)); + } + }; + + [impl() listWithMaxResults:max_results completion:completion_block]; + return ListLastResult(); +} + +Future StorageReferenceInternal::List(int32_t max_results, + const char* page_token) { + ReferenceCountedFutureImpl* future_impl = future(); + SafeFutureHandle handle = + future_impl->SafeAlloc(kStorageReferenceFnList); + StorageInternal* storage_internal = storage_; + + NSString* page_token_objc = page_token ? @(page_token) : nil; + + FIRStorageVoidListResultError completion_block = + ^(FIRStorageListResult* _Nullable list_result_objc, NSError* _Nullable error) { + Error error_code = NSErrorToErrorCode(error); + const char* error_message = GetErrorMessage(error_code); + if (list_result_objc != nil) { + auto list_internal = new ListResultInternal( + storage_internal, + std::make_unique(list_result_objc)); + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(list_internal)); + } else { + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(nullptr)); + } + }; + + [impl() listWithMaxResults:max_results + pageToken:page_token_objc + completion:completion_block]; + return ListLastResult(); +} + +Future StorageReferenceInternal::ListAll() { + ReferenceCountedFutureImpl* future_impl = future(); + SafeFutureHandle handle = + future_impl->SafeAlloc(kStorageReferenceFnList); + StorageInternal* storage_internal = storage_; + + FIRStorageVoidListResultError completion_block = + ^(FIRStorageListResult* _Nullable list_result_objc, NSError* _Nullable error) { + Error error_code = NSErrorToErrorCode(error); + const char* error_message = GetErrorMessage(error_code); + if (list_result_objc != nil) { + auto list_internal = new ListResultInternal( + storage_internal, + std::make_unique(list_result_objc)); + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(list_internal)); + } else { + future_impl->CompleteWithResult(handle, error_code, error_message, + ListResult(nullptr)); + } + }; + [impl() listAllWithCompletion:completion_block]; + return ListLastResult(); +} + +Future StorageReferenceInternal::ListLastResult() { + return static_cast&>( + future()->LastResult(kStorageReferenceFnList)); +} + ReferenceCountedFutureImpl* StorageReferenceInternal::future() { return storage_->future_manager().GetFutureApi(this); }